diff --git a/include/libssh/sk_common.h b/include/libssh/sk_common.h new file mode 100644 index 00000000..7c18448d --- /dev/null +++ b/include/libssh/sk_common.h @@ -0,0 +1,213 @@ +/* + * This file is part of the SSH Library + * + * Copyright (c) 2025 Praneeth Sarode + * + * 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 + +#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 */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 10b27b6c..5f1a711f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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") diff --git a/src/sk_common.c b/src/sk_common.c new file mode 100644 index 00000000..867a8a53 --- /dev/null +++ b/src/sk_common.c @@ -0,0 +1,276 @@ +/* + * This file is part of the SSH Library + * + * Copyright (c) 2025 Praneeth Sarode + * + * 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 +#include + +#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; +}