fido2: add helper functions for writing FIDO2/U2F callbacks

Add some common helper functions that can be used by any developer
writing callbacks for interacting with FIDO2/U2F devices.

Signed-off-by: Praneeth Sarode <praneethsarode@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
Reviewed-by: Eshan Kelkar <eshankelkar@galorithm.com>
This commit is contained in:
Praneeth Sarode
2025-07-05 18:39:12 +05:30
parent 8ba9e931e8
commit c1dd30b47b
3 changed files with 496 additions and 0 deletions

213
include/libssh/sk_common.h Normal file
View File

@@ -0,0 +1,213 @@
/*
* 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.
*/
#ifndef SK_COMMON_H
#define SK_COMMON_H
#include "libssh/callbacks.h"
#include "libssh/sk_api.h"
#include <stdbool.h>
#define SK_MAX_USER_ID_LEN 64
#define SK_NOT_SUPPORTED_MSG \
"Security Key functionality is not supported in this build of libssh. " \
"Please enable support by building using the WITH_FIDO2 build option."
/**
* @brief Convert security key error code to human-readable string
*
* Converts a security key error code to a descriptive string representation
* that can be used for logging user-facing error messages.
*
* @param[in] sk_err The security key error code to convert.
*
* @return Constant string describing the error. Never returns NULL.
* Returns "Unknown error" for unrecognized error codes.
*
* @note The returned string is statically allocated and should not be freed.
*/
const char *ssh_sk_err_to_string(int sk_err);
/**
* @brief Securely clear the contents of an sk_enroll_response structure
*
* Overwrites sensitive data within the enrollment response structure with
* zeros to prevent information leakage. This function only clears and frees the
* contents and does not free the structure itself.
*
* @param[in] enroll_response The enrollment response structure to clear.
* Can be NULL (no operation performed).
*
* @note This function only frees the memory for the contents and does not free
* memory for the structure itself. Use sk_enroll_response_free() for complete
* cleanup, which also performs secure clearing internally.
*/
void sk_enroll_response_burn(struct sk_enroll_response *enroll_response);
/**
* @brief Securely free an sk_enroll_response structure
*
* Performs secure clearing of sensitive data within the enrollment response
* structure before freeing the allocated memory. This function internally
* calls sk_enroll_response_burn() before deallocation.
*
* @param[in] enroll_response The enrollment response structure to free.
* Can be NULL (no operation performed).
*
* @note Developers do not need to call sk_enroll_response_burn() before
* calling this function, as secure clearing is performed automatically.
*/
void sk_enroll_response_free(struct sk_enroll_response *enroll_response);
/**
* @brief Free an sk_sign_response structure
*
* Frees the memory allocated for a sign response structure and all its
* associated data. This function performs secure clearing of sensitive
* data before deallocation.
*
* @param[in] sign_response The sign response structure to free.
* Can be NULL (no operation performed).
*
* @note This is a secure free operation that clears sensitive data before
* memory deallocation to prevent information leakage.
*/
void sk_sign_response_free(struct sk_sign_response *sign_response);
/**
* @brief Free an sk_resident_key structure
*
* Frees the memory allocated for a resident key structure and all its
* associated data. This function performs secure clearing of sensitive
* data before deallocation.
*
* @param[in] resident_key The resident key structure to free.
* Can be NULL (no operation performed).
*
* @note This is a secure free operation that clears sensitive data before
* memory deallocation to prevent information leakage.
*/
void sk_resident_key_free(struct sk_resident_key *resident_key);
/**
* @brief Free an sk_option array and all its contents
*
* Frees a NULL-terminated array of sk_option structures, including all
* allocated memory for option names and values within each structure.
*
* @param[in] options NULL-terminated array of sk_option pointers to free.
* Can be NULL (no operation performed).
*
* @note The options array must be NULL-terminated for proper freeing.
* Each sk_option structure and its name/value strings will be freed.
*/
void sk_options_free(struct sk_option **options);
/**
* @brief Validate options and extract values for specific keys
*
* Validates that all required options are supported and extracts values
* for the specified keys. This function is primarily intended for use
* by the SK callback implementations.
*
* @param[in] options NULL-terminated array of sk_option pointers to validate.
* @param[in] keys NULL-terminated array of supported option keys.
* @param[out] values Pointer to array that will be allocated and filled with
* copied values (same order as keys). The caller must free
* this array and all contained strings when done.
*
* @return SSH_OK on success, SSH_ERROR if unsupported required options found
* or memory allocation fails.
*
* @note The values array is allocated by this function and contains copies
* of the option values. The caller must free both the array and all
* non-NULL string values within it. Values for keys not found in
* options will be set to NULL.
*/
int sk_options_validate_get(const struct sk_option **options,
const char **keys,
char ***values);
/**
* @brief Duplicate an array of sk_option structures
*
* Creates a deep copy of an array of security key options. Each option
* structure and its string fields are duplicated.
*
* @param[in] options The array of options to duplicate. Must be
* NULL-terminated array of struct sk_option pointers.
* Can be NULL.
*
* @return A newly allocated array of duplicated options on success,
* NULL on failure or if options is NULL.
* The returned array should be freed with SK_OPTIONS_FREE().
*/
struct sk_option **sk_options_dup(const struct sk_option **options);
/**
* @brief Check version compatibility of security key callbacks
*
* Validates that the provided security key callbacks use an SK API
* version whose major portion is the same as the major version that libssh
* supports.
*
* @param[in] callbacks Pointer to the sk_callbacks structure to check.
*
* @return true if the callbacks are compatible, false otherwise.
*/
bool sk_callbacks_check_compatibility(
const struct ssh_sk_callbacks_struct *callbacks);
/* Convenience macros for secure freeing with NULL checks and pointer reset */
#define SK_ENROLL_RESPONSE_FREE(x) \
do { \
if ((x) != NULL) { \
sk_enroll_response_free(x); \
x = NULL; \
} \
} while (0)
#define SK_SIGN_RESPONSE_FREE(x) \
do { \
if ((x) != NULL) { \
sk_sign_response_free(x); \
x = NULL; \
} \
} while (0)
#define SK_RESIDENT_KEY_FREE(x) \
do { \
if ((x) != NULL) { \
sk_resident_key_free(x); \
x = NULL; \
} \
} while (0)
#define SK_OPTIONS_FREE(x) \
do { \
if ((x) != NULL) { \
sk_options_free(x); \
x = NULL; \
} \
} while (0)
#endif /* SK_COMMON_H */

View File

@@ -305,6 +305,13 @@ if (HAVE_MLKEM)
)
endif (HAVE_MLKEM)
if (WITH_FIDO2)
set(libssh_SRCS
${libssh_SRCS}
sk_common.c
)
endif (WITH_FIDO2)
# Set the path to the default map file
set(MAP_PATH "${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}.map")

276
src/sk_common.c Normal file
View File

@@ -0,0 +1,276 @@
/*
* 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 <stdlib.h>
#include <string.h>
#include "libssh/priv.h"
#include "libssh/sk_common.h"
const char *ssh_sk_err_to_string(int sk_err)
{
switch (sk_err) {
case SSH_SK_ERR_UNSUPPORTED:
return "Unsupported operation";
case SSH_SK_ERR_PIN_REQUIRED:
return "PIN required but is either missing or invalid";
case SSH_SK_ERR_DEVICE_NOT_FOUND:
return "No suitable device found";
case SSH_SK_ERR_CREDENTIAL_EXISTS:
return "Credential already exists";
case SSH_SK_ERR_GENERAL:
return "General error";
default:
return "Unknown error";
}
}
void sk_enroll_response_burn(struct sk_enroll_response *enroll_response)
{
if (enroll_response == NULL) {
return;
}
BURN_FREE(enroll_response->public_key, enroll_response->public_key_len);
BURN_FREE(enroll_response->key_handle, enroll_response->key_handle_len);
BURN_FREE(enroll_response->signature, enroll_response->signature_len);
BURN_FREE(enroll_response->attestation_cert,
enroll_response->attestation_cert_len);
BURN_FREE(enroll_response->authdata, enroll_response->authdata_len);
explicit_bzero(enroll_response, sizeof(*enroll_response));
}
void sk_enroll_response_free(struct sk_enroll_response *enroll_response)
{
sk_enroll_response_burn(enroll_response);
SAFE_FREE(enroll_response);
}
void sk_sign_response_free(struct sk_sign_response *sign_response)
{
if (sign_response == NULL) {
return;
}
BURN_FREE(sign_response->sig_r, sign_response->sig_r_len);
BURN_FREE(sign_response->sig_s, sign_response->sig_s_len);
SAFE_FREE(sign_response);
}
void sk_resident_key_free(struct sk_resident_key *resident_key)
{
if (resident_key == NULL) {
return;
}
SAFE_FREE(resident_key->application);
BURN_FREE(resident_key->user_id, resident_key->user_id_len);
sk_enroll_response_burn(&resident_key->key);
SAFE_FREE(resident_key);
}
void sk_options_free(struct sk_option **options)
{
size_t i;
if (options == NULL) {
return;
}
for (i = 0; options[i] != NULL; i++) {
SAFE_FREE(options[i]->name);
SAFE_FREE(options[i]->value);
SAFE_FREE(options[i]);
}
SAFE_FREE(options);
}
int sk_options_validate_get(const struct sk_option **options,
const char **keys,
char ***values)
{
size_t i, j;
size_t key_count = 0;
int found;
if (keys == NULL || values == NULL || options == NULL) {
SSH_LOG(SSH_LOG_WARN, "Invalid parameter(s) provided");
return SSH_ERROR;
}
while (keys[key_count] != NULL) {
key_count++;
}
*values = calloc(key_count + 1, sizeof(char *));
if (*values == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate values array");
return SSH_ERROR;
}
for (i = 0; options[i] != NULL; i++) {
const struct sk_option *option = options[i];
found = 0;
/* Look for this option name in the supported keys */
for (j = 0; j < key_count; j++) {
if (strcmp(option->name, keys[j]) == 0) {
/* Copy the value string if it exists */
if (option->value != NULL) {
(*values)[j] = strdup(option->value);
if ((*values)[j] == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to copy option value");
goto error;
}
} else {
(*values)[j] = NULL;
}
found = 1;
break;
}
}
/* If option is required but not supported, fail */
if (!found && option->required) {
SSH_LOG(SSH_LOG_WARN,
"Required option '%s' is not supported",
option->name);
goto error;
}
}
return SSH_OK;
error:
for (j = 0; j < key_count; j++) {
SAFE_FREE((*values)[j]);
}
SAFE_FREE(*values);
return SSH_ERROR;
}
struct sk_option **sk_options_dup(const struct sk_option **options)
{
struct sk_option **new_options = NULL;
size_t count = 0;
size_t i;
if (options == NULL) {
return NULL;
}
/* Count the number of options */
while (options[count] != NULL) {
count++;
}
new_options = calloc(count + 1, sizeof(struct sk_option *));
if (new_options == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for options array");
return NULL;
}
/* Copy each option */
for (i = 0; i < count; i++) {
const struct sk_option *option = options[i];
struct sk_option *new_option = NULL;
new_option = calloc(1, sizeof(struct sk_option));
if (new_option == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for option");
goto error;
}
/* Copy option name */
if (option->name != NULL) {
new_option->name = strdup(option->name);
if (new_option->name == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to copy option name");
SAFE_FREE(new_option);
goto error;
}
}
/* Copy option value */
if (option->value != NULL) {
new_option->value = strdup(option->value);
if (new_option->value == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to copy option value");
SAFE_FREE(new_option->name);
SAFE_FREE(new_option);
goto error;
}
}
new_option->required = option->required;
new_options[i] = new_option;
}
new_options[count] = NULL;
return new_options;
error:
SK_OPTIONS_FREE(new_options);
return NULL;
}
bool sk_callbacks_check_compatibility(
const struct ssh_sk_callbacks_struct *callbacks)
{
uint32_t callback_version;
uint32_t callback_version_major;
uint32_t libssh_version_major;
if (callbacks == NULL) {
SSH_LOG(SSH_LOG_WARN, "SK callbacks cannot be NULL");
return false;
}
/* Check if the api_version callback is provided */
if (!ssh_callbacks_exists(callbacks, api_version)) {
SSH_LOG(SSH_LOG_WARN, "SK callbacks missing api_version callback");
return false;
}
/* Extract major version from callback provider */
callback_version = callbacks->api_version();
callback_version_major = callback_version & SSH_SK_VERSION_MAJOR_MASK;
libssh_version_major = SSH_SK_VERSION_MAJOR;
/* Check if major versions are compatible */
if (callback_version_major != libssh_version_major) {
SSH_LOG(SSH_LOG_WARN,
"SK API major version mismatch: callback provides 0x%08x, "
"libssh supports 0x%08x",
callback_version_major,
libssh_version_major);
return false;
}
return true;
}