feat(pki): add security key support with enrollment, signing, and resident key loading functions

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-09-20 19:34:41 +05:30
parent 32a256e157
commit 37f0e91814
6 changed files with 1264 additions and 22 deletions

View File

@@ -727,8 +727,13 @@ LIBSSH_API uint32_t ssh_key_get_sk_flags(const ssh_key key);
LIBSSH_API ssh_string ssh_key_get_sk_application(const ssh_key key);
LIBSSH_API ssh_string ssh_key_get_sk_user_id(const ssh_key key);
LIBSSH_API int ssh_pki_generate(enum ssh_keytypes_e type, int parameter,
ssh_key *pkey);
SSH_DEPRECATED LIBSSH_API int
ssh_pki_generate(enum ssh_keytypes_e type, int parameter, ssh_key *pkey);
LIBSSH_API int ssh_pki_generate_key(enum ssh_keytypes_e type,
ssh_pki_ctx pki_context,
ssh_key *pkey);
LIBSSH_API int ssh_pki_import_privkey_base64(const char *b64_key,
const char *passphrase,
ssh_auth_callback auth_fn,
@@ -929,6 +934,29 @@ enum ssh_pki_options_e {
SSH_PKI_OPTION_SK_CALLBACKS,
};
/* FIDO2/U2F Operation Flags */
/** Requires user presence confirmation (tap/touch) */
#ifndef SSH_SK_USER_PRESENCE_REQD
#define SSH_SK_USER_PRESENCE_REQD 0x01
#endif
/** Requires user verification (PIN/biometric) - FIDO2 only */
#ifndef SSH_SK_USER_VERIFICATION_REQD
#define SSH_SK_USER_VERIFICATION_REQD 0x04
#endif
/** Force resident key enrollment even if a resident key with given user ID
* already exists - FIDO2 only */
#ifndef SSH_SK_FORCE_OPERATION
#define SSH_SK_FORCE_OPERATION 0x10
#endif
/** Create/use resident key stored on authenticator - FIDO2 only */
#ifndef SSH_SK_RESIDENT_KEY
#define SSH_SK_RESIDENT_KEY 0x20
#endif
LIBSSH_API ssh_pki_ctx ssh_pki_ctx_new(void);
LIBSSH_API int ssh_pki_ctx_options_set(ssh_pki_ctx context,
@@ -963,6 +991,13 @@ LIBSSH_API void ssh_pki_ctx_free(ssh_pki_ctx context);
} \
} while (0)
/* Security key resident keys API */
LIBSSH_API int
ssh_sk_resident_keys_load(const struct ssh_pki_ctx_struct *pki_context,
ssh_key **resident_keys_result,
size_t *num_keys_found_result);
#ifndef LIBSSH_LEGACY_0_4
#include "libssh/legacy.h"
#endif

90
include/libssh/pki_sk.h Normal file
View File

@@ -0,0 +1,90 @@
/*
* 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 PKI_SK_H
#define PKI_SK_H
#include "libssh/libssh.h"
#include "libssh/pki.h"
#include <stdint.h>
#define SSH_SK_MAX_USER_ID_LEN 64
/**
* @brief Enroll a new security key using a U2F/FIDO2 authenticator
*
* Creates a new security key credential configured according to the parameters
* in the PKI context. This function handles key enrollment for both ECDSA and
* Ed25519 algorithms, generates appropriate challenges, and returns the
* enrolled key with optional attestation data.
*
* The PKI context must be configured with appropriate security key parameters
* using ssh_pki_ctx_options_set() before calling this function. Required
* options include SSH_PKI_OPTION_SK_APPLICATION, SSH_PKI_OPTION_SK_USER_ID, and
* SSH_PKI_OPTION_SK_CALLBACKS.
*
* @param[in] context The PKI context containing security key configuration and
* parameters
* @param[in] key_type The type of key to enroll (SSH_KEYTYPE_SK_ECDSA or
* SSH_KEYTYPE_SK_ED25519)
* @param[out] enrolled_key_result Pointer to store the enrolled ssh_key
*
* @return SSH_OK on success, SSH_ERROR on failure
*
* @see ssh_pki_ctx_new()
* @see ssh_pki_ctx_options_set()
* @see ssh_pki_ctx_get_sk_attestation_buffer()
*/
int pki_sk_enroll_key(ssh_pki_ctx context,
enum ssh_keytypes_e key_type,
ssh_key *enrolled_key_result);
/**
* @brief Sign arbitrary data using a security key and a PKI context
*
* This function performs signing operations configured according to the
* parameters in the PKI context and returns a properly formatted
* ssh_signature. The caller must free the signature when it is no longer
* needed.
*
* The PKI context should be configured with appropriate security key parameters
* using ssh_pki_ctx_options_set() before calling this function. The security
* key must have been previously enrolled or loaded.
*
* @param[in] context The PKI context containing security key configuration and
* parameters
* @param[in] key The security key to use for signing
* @param[in] data The data to sign
* @param[in] data_len Length of data to sign
*
* @return A valid ssh_signature on success, NULL on failure
*
* @see ssh_pki_ctx_new()
* @see ssh_pki_ctx_options_set()
* @see pki_sk_enroll_key()
* @see ssh_signature_free()
*/
ssh_signature pki_sk_do_sign(ssh_pki_ctx context,
const ssh_key key,
const uint8_t *data,
size_t data_len);
#endif /* PKI_SK_H */

View File

@@ -310,6 +310,7 @@ if (WITH_FIDO2)
set(libssh_SRCS
${libssh_SRCS}
sk_common.c
pki_sk.c
)
if (HAVE_LIBFIDO2)

View File

@@ -503,4 +503,6 @@ LIBSSH_AFTER_4_10_0
ssh_key_get_sk_flags;
ssh_key_get_sk_application;
ssh_key_get_sk_user_id;
ssh_pki_generate_key;
ssh_sk_resident_keys_load;
} LIBSSH_4_10_0;

183
src/pki.c
View File

@@ -43,15 +43,18 @@
#include <sys/stat.h>
#include <sys/types.h>
#include "libssh/libssh.h"
#include "libssh/session.h"
#include "libssh/priv.h"
#include "libssh/pki.h"
#include "libssh/pki_priv.h"
#include "libssh/keys.h"
#include "libssh/buffer.h"
#include "libssh/misc.h"
#include "libssh/agent.h"
#include "libssh/buffer.h"
#include "libssh/keys.h"
#include "libssh/libssh.h"
#include "libssh/misc.h"
#include "libssh/pki.h"
#include "libssh/pki_context.h"
#include "libssh/pki_priv.h"
#include "libssh/pki_sk.h"
#include "libssh/priv.h"
#include "libssh/session.h"
#include "libssh/sk_common.h" /* For SK_NOT_SUPPORTED_MSG */
#ifndef MAX_LINE_SIZE
#define MAX_LINE_SIZE 4096
@@ -2257,7 +2260,9 @@ int ssh_pki_import_cert_file(const char *filename, ssh_key *pkey)
}
/**
* @brief Generates a key pair.
* @internal
*
* @brief Internal function to generate a key pair.
*
* @param[in] type Type of key to create
*
@@ -2268,17 +2273,19 @@ int ssh_pki_import_cert_file(const char *filename, ssh_key *pkey)
* to free the memory using ssh_key_free().
*
* @return SSH_OK on success, SSH_ERROR on error.
*
* @warning Generating a key pair may take some time.
*
* @see ssh_key_free()
*/
int ssh_pki_generate(enum ssh_keytypes_e type, int parameter,
ssh_key *pkey)
static int pki_generate_key_internal(enum ssh_keytypes_e type,
int parameter,
ssh_key *pkey)
{
int rc;
ssh_key key = ssh_key_new();
ssh_key key = NULL;
if (pkey == NULL) {
return SSH_ERROR;
}
key = ssh_key_new();
if (key == NULL) {
return SSH_ERROR;
}
@@ -2289,6 +2296,15 @@ int ssh_pki_generate(enum ssh_keytypes_e type, int parameter,
switch(type){
case SSH_KEYTYPE_RSA:
if (parameter != 0 && parameter < RSA_MIN_KEY_SIZE) {
SSH_LOG(
SSH_LOG_WARN,
"RSA key size parameter (%d) is below minimum allowed (%d)",
parameter,
RSA_MIN_KEY_SIZE);
goto error;
}
rc = pki_key_generate_rsa(key, parameter);
if(rc == SSH_ERROR)
goto error;
@@ -2350,6 +2366,105 @@ error:
return SSH_ERROR;
}
/**
* @brief Generates a key pair.
*
* @param[in] type Type of key to create
*
* @param[in] parameter Parameter to the creation of key:
* rsa : length of the key in bits (e.g. 1024, 2048, 4096)
* If parameter is 0, then the default size will be used.
* @param[out] pkey A pointer to store the allocated private key. You need
* to free the memory using ssh_key_free().
*
* @return SSH_OK on success, SSH_ERROR on error.
*
* @warning Generating a key pair may take some time.
*
* @see ssh_key_free()
*/
int ssh_pki_generate(enum ssh_keytypes_e type, int parameter, ssh_key *pkey)
{
return pki_generate_key_internal(type, parameter, pkey);
}
/**
* @brief Generates a key pair.
*
* @param[in] type Type of key to create
*
* @param[in] pki_context PKI context containing various configuration
* parameters and sub-contexts. Can be NULL for
* standard SSH key types (RSA, ECDSA, ED25519) where
* defaults will be used. Can also be NULL for security
* key types (SK_*), in which case default callbacks and
* settings will be used automatically.
*
* @param[out] pkey A pointer to store the allocated private key. You need
* to free the memory using ssh_key_free().
*
* @return SSH_OK on success, SSH_ERROR on error.
*
* @see ssh_pki_ctx_new()
* @see ssh_key_free()
*/
int ssh_pki_generate_key(enum ssh_keytypes_e type,
ssh_pki_ctx pki_context,
ssh_key *pkey)
{
/* Handle Security Key types with the specialized function */
if (is_sk_key_type(type)) {
#ifdef WITH_FIDO2
ssh_pki_ctx temp_ctx = NULL;
ssh_pki_ctx ctx_to_use = pki_context;
int rc;
/* If no context provided, create a temporary default one */
if (pki_context == NULL) {
SSH_LOG(SSH_LOG_INFO,
"No PKI context provided, using the default one");
temp_ctx = ssh_pki_ctx_new();
if (temp_ctx == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create temporary PKI context");
return SSH_ERROR;
}
ctx_to_use = temp_ctx;
}
/* Verify that we have valid SK callbacks */
if (ctx_to_use->sk_callbacks == NULL) {
SSH_LOG(SSH_LOG_WARN, "Missing SK callbacks in PKI context");
if (temp_ctx != NULL) {
SSH_PKI_CTX_FREE(temp_ctx);
}
return SSH_ERROR;
}
rc = pki_sk_enroll_key(ctx_to_use, type, pkey);
/* Clean up temporary context if we created one */
if (temp_ctx != NULL) {
SSH_PKI_CTX_FREE(temp_ctx);
}
return rc;
#else /* WITH_FIDO2 */
SSH_LOG(SSH_LOG_WARN, SK_NOT_SUPPORTED_MSG);
return SSH_ERROR;
#endif /* WITH_FIDO2 */
} else {
int parameter = 0;
if (type == SSH_KEYTYPE_RSA && pki_context != NULL) {
parameter = pki_context->rsa_key_size;
}
return pki_generate_key_internal(type, parameter, pkey);
}
}
/**
* @brief Create a public key from a private key.
*
@@ -3636,10 +3751,38 @@ ssh_string ssh_pki_do_sign(ssh_session session,
}
/* Generate the signature */
sig = pki_do_sign(privkey,
ssh_buffer_get(sign_input),
ssh_buffer_get_len(sign_input),
hash_type);
if (is_sk_key_type(privkey->type)) {
#ifdef WITH_FIDO2
if (session->pki_context == NULL ||
session->pki_context->sk_callbacks == NULL) {
SSH_LOG(SSH_LOG_WARN, "Missing PKI context or SK callbacks");
goto end;
}
rc = pki_key_check_hash_compatible(privkey, hash_type);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN,
"Incompatible hash type %d for sk key type %d",
hash_type,
privkey->type);
goto end;
}
sig = pki_sk_do_sign(session->pki_context,
privkey,
ssh_buffer_get(sign_input),
ssh_buffer_get_len(sign_input));
#else
SSH_LOG(SSH_LOG_WARN, SK_NOT_SUPPORTED_MSG);
goto end;
#endif /* WITH_FIDO2 */
} else {
sig = pki_do_sign(privkey,
ssh_buffer_get(sign_input),
ssh_buffer_get_len(sign_input),
hash_type);
}
if (sig == NULL) {
goto end;
}

971
src/pki_sk.c Normal file
View File

@@ -0,0 +1,971 @@
/*
* 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/buffer.h"
#include "libssh/pki_context.h"
#include "libssh/pki_priv.h"
#include "libssh/pki_sk.h"
#include "libssh/sk_common.h"
#include <stdint.h>
#include <string.h>
#define DEFAULT_PIN_PROMPT "Enter SK PIN: "
#define PIN_BUF_SIZE 64
/**
* @addtogroup libssh_pki
* @{
*/
/**
* @brief Serialize FIDO2 attestation data into an SSH buffer
*
* Serializes the attestation certificate, signature, and authenticator data
* from a FIDO2 enrollment response into an SSH buffer in the
* "ssh-sk-attest-v01" format.
*
* @param[in] enroll_response The sk_enroll_response struct containing
* attestation data from FIDO2 enrollment
* @param[in,out] attestation_buffer SSH buffer to store the serialized
* attestation data
*
* @return SSH_OK on success, SSH_ERROR on failure
*/
static int pki_sk_serialise_attestation_cert(
const struct sk_enroll_response *enroll_response,
ssh_buffer attestation_buffer)
{
int rc;
if (attestation_buffer == NULL || enroll_response == NULL) {
SSH_LOG(SSH_LOG_WARN, "Parameters cannot be NULL");
return SSH_ERROR;
}
/* Check if attestation data is available */
if (enroll_response->attestation_cert == NULL ||
enroll_response->attestation_cert_len == 0) {
SSH_LOG(SSH_LOG_INFO, "No attestation certificate available");
return SSH_ERROR;
}
if (enroll_response->signature == NULL ||
enroll_response->signature_len == 0) {
SSH_LOG(SSH_LOG_INFO, "No attestation signature available");
return SSH_ERROR;
}
if (enroll_response->authdata == NULL ||
enroll_response->authdata_len == 0) {
SSH_LOG(SSH_LOG_INFO, "No authenticator data available");
return SSH_ERROR;
}
rc = ssh_buffer_pack(attestation_buffer,
"sdPdPdPds",
"ssh-sk-attest-v01",
(uint32_t)enroll_response->attestation_cert_len,
enroll_response->attestation_cert_len,
enroll_response->attestation_cert,
(uint32_t)enroll_response->signature_len,
enroll_response->signature_len,
enroll_response->signature,
(uint32_t)enroll_response->authdata_len,
enroll_response->authdata_len,
enroll_response->authdata,
(uint32_t)0, /* reserved flags */
""); /* reserved */
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to pack attestation data into buffer");
return SSH_ERROR;
}
return SSH_OK;
}
/**
* @brief Create an ssh_key from an sk_enroll_response struct
*
* Constructs an ssh_key structure from an sk_enroll_response
* struct for both ECDSA and Ed25519 algorithms.
*
* @param[in] algorithm The algorithm type (SSH_SK_ECDSA or
* SSH_SK_ED25519)
* @param[in] application The application string (relying party ID)
* @param[in] enroll_response The sk_enroll_response struct containing key data
* @param[out] ssh_key_result Pointer to store the newly created ssh_key
*
* @return SSH_OK on success, SSH_ERROR on failure
*/
static int pki_sk_enroll_response_to_ssh_key(
int algorithm,
const char *application,
const struct sk_enroll_response *enroll_response,
ssh_key *ssh_key_result)
{
ssh_key key_to_build = NULL;
ssh_string public_key_string = NULL;
int rc, ret = SSH_ERROR;
/* Validate input parameters */
if (ssh_key_result == NULL) {
SSH_LOG(SSH_LOG_WARN, "ssh_key pointer cannot be NULL");
return SSH_ERROR;
}
*ssh_key_result = NULL;
if (enroll_response == NULL) {
SSH_LOG(SSH_LOG_WARN, "Enrollment response cannot be NULL");
return SSH_ERROR;
}
/* Validate response data */
if (enroll_response->public_key == NULL ||
enroll_response->key_handle == NULL) {
SSH_LOG(
SSH_LOG_WARN,
"Invalid enrollment response: missing public key or key handle");
return SSH_ERROR;
}
key_to_build = ssh_key_new();
if (key_to_build == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate new ssh_key");
return SSH_ERROR;
}
/* Set key type based on algorithm */
switch (algorithm) {
#ifdef HAVE_ECC
case SSH_SK_ECDSA:
key_to_build->type = SSH_KEYTYPE_SK_ECDSA;
break;
#endif /* HAVE_ECC */
case SSH_SK_ED25519:
key_to_build->type = SSH_KEYTYPE_SK_ED25519;
break;
default:
SSH_LOG(SSH_LOG_WARN, "Unsupported algorithm: %d", algorithm);
goto out;
}
key_to_build->type_c = ssh_key_type_to_char(key_to_build->type);
public_key_string = ssh_string_from_data(enroll_response->public_key,
enroll_response->public_key_len);
if (public_key_string == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create public key string");
goto out;
}
switch (algorithm) {
#ifdef HAVE_ECC
case SSH_SK_ECDSA:
rc = pki_pubkey_build_ecdsa(key_to_build,
pki_key_ecdsa_nid_from_name("nistp256"),
public_key_string);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to build ECDSA public key");
goto out;
}
break;
#endif /* HAVE_ECC */
case SSH_SK_ED25519:
rc = pki_pubkey_build_ed25519(key_to_build, public_key_string);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to build ED25519 public key");
goto out;
}
break;
}
/* Set security key specific fields */
key_to_build->sk_application = ssh_string_from_char(application);
if (key_to_build->sk_application == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create sk_application string");
goto out;
}
/* Set key handle */
key_to_build->sk_key_handle =
ssh_string_from_data(enroll_response->key_handle,
enroll_response->key_handle_len);
if (key_to_build->sk_key_handle == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create sk_key_handle string");
goto out;
}
key_to_build->sk_reserved = ssh_string_from_data(NULL, 0);
if (key_to_build->sk_reserved == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create sk_reserved string");
goto out;
}
key_to_build->sk_flags = enroll_response->flags;
key_to_build->flags = SSH_KEY_FLAG_PRIVATE | SSH_KEY_FLAG_PUBLIC;
*ssh_key_result = key_to_build;
key_to_build = NULL;
ret = SSH_OK;
out:
ssh_string_burn(public_key_string);
SSH_STRING_FREE(public_key_string);
SSH_KEY_FREE(key_to_build);
return ret;
}
int pki_sk_enroll_key(ssh_pki_ctx context,
enum ssh_keytypes_e key_type,
ssh_key *enrolled_key_result)
{
const struct ssh_sk_callbacks_struct *sk_callbacks = NULL;
struct sk_enroll_response *enroll_response = NULL;
ssh_key enrolled_key = NULL;
char pin_buf[PIN_BUF_SIZE] = {0};
const char *pin_to_use = NULL;
unsigned char random_challenge[32];
const unsigned char *challenge = NULL;
size_t challenge_length = 0;
ssh_buffer challenge_buffer = NULL;
ssh_buffer attestation = NULL;
int rc, ret = SSH_ERROR;
int algorithm;
/* Validate input parameters */
if (context == NULL) {
SSH_LOG(SSH_LOG_WARN, "SK context cannot be NULL");
return SSH_ERROR;
}
if (enrolled_key_result == NULL) {
SSH_LOG(SSH_LOG_WARN, "Enrolled key result pointer cannot be NULL");
return SSH_ERROR;
}
/* Initialize output parameter */
*enrolled_key_result = NULL;
/* Clear any existing attestation data */
SSH_BUFFER_FREE(context->sk_attestation_buffer);
/* Get security key callbacks from context */
sk_callbacks = context->sk_callbacks;
if (sk_callbacks == NULL) {
SSH_LOG(SSH_LOG_WARN, "Security key callbacks cannot be NULL");
return SSH_ERROR;
}
if (!ssh_callbacks_exists(sk_callbacks, enroll)) {
SSH_LOG(SSH_LOG_WARN,
"Security key enroll callback is not implemented");
return SSH_ERROR;
}
/* Validate required fields */
if (context->sk_application == NULL || *context->sk_application == '\0') {
SSH_LOG(SSH_LOG_WARN, "Application identifier cannot be NULL or empty");
return SSH_ERROR;
}
/* Extract parameters from context */
challenge_buffer = context->sk_challenge_buffer;
/* Determine algorithm based on key type */
switch (key_type) {
#ifdef HAVE_ECC
case SSH_KEYTYPE_SK_ECDSA:
algorithm = SSH_SK_ECDSA;
break;
#endif /* HAVE_ECC */
case SSH_KEYTYPE_SK_ED25519:
algorithm = SSH_SK_ED25519;
break;
default:
SSH_LOG(SSH_LOG_WARN,
"Unsupported key type for security key enrollment");
goto out;
}
/* Determine challenge to use */
if (challenge_buffer == NULL) {
SSH_LOG(SSH_LOG_DEBUG, "Using randomly generated challenge");
rc = ssh_get_random(random_challenge, sizeof(random_challenge), 0);
if (rc != 1) {
SSH_LOG(SSH_LOG_WARN, "Failed to generate random challenge");
goto out;
}
challenge = random_challenge;
challenge_length = sizeof(random_challenge);
} else {
challenge_length = ssh_buffer_get_len(challenge_buffer);
if (challenge_length == 0) {
SSH_LOG(SSH_LOG_WARN, "Challenge buffer cannot be empty");
goto out;
}
challenge = ssh_buffer_get(challenge_buffer);
SSH_LOG(SSH_LOG_DEBUG,
"Using provided challenge of length %zu",
challenge_length);
}
if (context->sk_pin_callback != NULL) {
rc = context->sk_pin_callback(DEFAULT_PIN_PROMPT,
pin_buf,
sizeof(pin_buf),
0,
0,
context->sk_userdata);
if (rc == SSH_OK) {
pin_to_use = pin_buf;
} else {
SSH_LOG(SSH_LOG_WARN, "Failed to fetch PIN from callback");
explicit_bzero(pin_buf, sizeof(pin_buf));
goto out;
}
} else {
SSH_LOG(SSH_LOG_INFO, "Trying operation without PIN");
}
rc = sk_callbacks->enroll(algorithm,
challenge,
challenge_length,
context->sk_application,
context->sk_flags,
pin_to_use,
context->sk_callbacks_options,
&enroll_response);
explicit_bzero(pin_buf, sizeof(pin_buf));
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN,
"Security key enroll callback failed: %s (%d)",
ssh_sk_err_to_string(rc),
rc);
goto out;
}
/* Convert SK enroll response to ssh_key */
rc = pki_sk_enroll_response_to_ssh_key(algorithm,
context->sk_application,
enroll_response,
&enrolled_key);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to convert enroll response to ssh_key");
goto out;
}
/* Try to serialize attestation data and store in context */
attestation = ssh_buffer_new();
if (attestation == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate attestation buffer");
goto out;
} else {
rc = pki_sk_serialise_attestation_cert(enroll_response, attestation);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_INFO,
"Failed to serialize attestation data, continuing without "
"attestation");
} else {
context->sk_attestation_buffer = attestation;
attestation = NULL;
}
}
*enrolled_key_result = enrolled_key;
enrolled_key = NULL;
ret = SSH_OK;
out:
if (challenge == random_challenge) {
explicit_bzero(random_challenge, sizeof(random_challenge));
}
SK_ENROLL_RESPONSE_FREE(enroll_response);
SSH_KEY_FREE(enrolled_key);
SSH_BUFFER_FREE(attestation);
return ret;
}
static int
pki_sk_pack_ecdsa_signature(const struct sk_sign_response *sign_response,
ssh_buffer sig_buffer)
{
bignum r_bn = NULL, s_bn = NULL;
ssh_buffer inner_buffer = NULL;
int rc = SSH_ERROR;
/* Convert raw r and s bytes to bignums */
bignum_bin2bn(sign_response->sig_r, (int)sign_response->sig_r_len, &r_bn);
if (r_bn == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to convert sig_r to bignum");
goto out;
}
bignum_bin2bn(sign_response->sig_s, (int)sign_response->sig_s_len, &s_bn);
if (s_bn == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to convert sig_s to bignum");
goto out;
}
/* Create inner buffer with r and s as SSH strings */
inner_buffer = ssh_buffer_new();
if (inner_buffer == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create inner buffer");
goto out;
}
ssh_buffer_set_secure(inner_buffer);
rc = ssh_buffer_pack(inner_buffer, "BB", r_bn, s_bn);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to pack r and s into inner buffer");
goto out;
}
rc = ssh_buffer_pack(sig_buffer,
"P",
(size_t)ssh_buffer_get_len(inner_buffer),
ssh_buffer_get(inner_buffer));
if (rc != SSH_OK) {
goto out;
}
rc = SSH_OK;
out:
SSH_BUFFER_FREE(inner_buffer);
bignum_safe_free(s_bn);
bignum_safe_free(r_bn);
return rc;
}
static int
pki_sk_pack_ed25519_signature(const struct sk_sign_response *sign_response,
ssh_buffer sig_buffer)
{
int rc = SSH_ERROR;
rc = ssh_buffer_pack(sig_buffer,
"P",
sign_response->sig_r_len,
sign_response->sig_r);
if (rc != SSH_OK) {
return SSH_ERROR;
}
return SSH_OK;
}
/**
* @brief Create an ssh_signature from a sk_sign_response structure
*
* Serializes a security key sign response into an ssh_signature structure
* for both ECDSA and Ed25519 algorithms.
*
* @param[in] algorithm The algorithm used (SSH_SK_ECDSA or SSH_SK_ED25519)
* @param[in] key_type The SSH key type for setting signature type
* @param[in] sign_response The sk_sign_response containing signature data
* @param[out] ssh_signature_result Pointer to store the created ssh_signature
*
* @return SSH_OK on success, SSH_ERROR on failure
*/
static int pki_sk_sign_response_to_ssh_signature(
int algorithm,
enum ssh_keytypes_e key_type,
const struct sk_sign_response *sign_response,
ssh_signature *ssh_signature_result)
{
ssh_signature signature_to_build = NULL;
ssh_buffer sig_buffer = NULL;
int rc;
/* Validate input parameters */
if (ssh_signature_result == NULL) {
SSH_LOG(SSH_LOG_WARN, "ssh_signature pointer cannot be NULL");
return SSH_ERROR;
}
*ssh_signature_result = NULL;
if (sign_response == NULL) {
SSH_LOG(SSH_LOG_WARN, "Sign response cannot be NULL");
return SSH_ERROR;
}
/* Validate response data based on algorithm */
switch (algorithm) {
#ifdef HAVE_ECC
case SSH_SK_ECDSA:
if (sign_response->sig_r == NULL || sign_response->sig_s == NULL) {
SSH_LOG(SSH_LOG_WARN,
"Invalid ECDSA sign response: missing sig_r or sig_s");
return SSH_ERROR;
}
break;
#endif /* HAVE_ECC */
case SSH_SK_ED25519:
if (sign_response->sig_r == NULL ||
sign_response->sig_r_len != ED25519_SIG_LEN) {
SSH_LOG(SSH_LOG_WARN, "Invalid sig_r in Ed25519 sign response");
return SSH_ERROR;
}
break;
default:
SSH_LOG(SSH_LOG_WARN, "Unsupported algorithm: %d", algorithm);
return SSH_ERROR;
}
/* Create new ssh_signature */
signature_to_build = ssh_signature_new();
if (signature_to_build == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate new ssh_signature");
return SSH_ERROR;
}
/* Set signature type and metadata */
signature_to_build->type = key_type;
signature_to_build->type_c = ssh_key_type_to_char(key_type);
/* Set security key specific fields */
signature_to_build->sk_flags = sign_response->flags;
signature_to_build->sk_counter = sign_response->counter;
/* Create a buffer to hold the signature data */
sig_buffer = ssh_buffer_new();
if (sig_buffer == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create signature buffer");
goto error;
}
ssh_buffer_set_secure(sig_buffer);
/* Build the signature based on algorithm */
switch (algorithm) {
#ifdef HAVE_ECC
case SSH_SK_ECDSA:
signature_to_build->hash_type = SSH_DIGEST_SHA256;
rc = pki_sk_pack_ecdsa_signature(sign_response, sig_buffer);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to pack ECDSA signature");
goto error;
}
break;
#endif /* HAVE_ECC */
case SSH_SK_ED25519:
signature_to_build->hash_type = SSH_DIGEST_AUTO;
rc = pki_sk_pack_ed25519_signature(sign_response, sig_buffer);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to pack Ed25519 signature");
goto error;
}
break;
}
/* Set the signature data */
signature_to_build->raw_sig =
ssh_string_from_data(ssh_buffer_get(sig_buffer),
ssh_buffer_get_len(sig_buffer));
if (signature_to_build->raw_sig == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create raw signature string");
goto error;
}
*ssh_signature_result = signature_to_build;
SSH_BUFFER_FREE(sig_buffer);
return SSH_OK;
error:
SSH_SIGNATURE_FREE(signature_to_build);
SSH_BUFFER_FREE(sig_buffer);
return SSH_ERROR;
}
ssh_signature pki_sk_do_sign(ssh_pki_ctx context,
const ssh_key key,
const unsigned char *data,
size_t data_len)
{
const struct ssh_sk_callbacks_struct *sk_callbacks = NULL;
struct sk_sign_response *sign_response = NULL;
ssh_signature signature = NULL;
char pin_buf[PIN_BUF_SIZE] = {0};
const char *pin_to_use = NULL;
int algorithm;
int rc = SSH_ERROR;
/* Validate input parameters */
if (context == NULL) {
SSH_LOG(SSH_LOG_WARN, "Context cannot be NULL");
return NULL;
}
/* Get security key callbacks from context */
sk_callbacks = context->sk_callbacks;
if (sk_callbacks == NULL) {
SSH_LOG(SSH_LOG_WARN, "Security key callbacks cannot be NULL");
return NULL;
}
if (!ssh_callbacks_exists(sk_callbacks, sign)) {
SSH_LOG(SSH_LOG_WARN, "Security key sign callback is not implemented");
return NULL;
}
if (key == NULL) {
SSH_LOG(SSH_LOG_WARN, "Key cannot be NULL");
return NULL;
}
if (data == NULL || data_len == 0) {
SSH_LOG(SSH_LOG_WARN, "Data cannot be NULL or empty");
return NULL;
}
/* Validate key type and determine algorithm */
switch (key->type) {
#ifdef HAVE_ECC
case SSH_KEYTYPE_SK_ECDSA:
algorithm = SSH_SK_ECDSA;
break;
#endif /* HAVE_ECC */
case SSH_KEYTYPE_SK_ED25519:
algorithm = SSH_SK_ED25519;
break;
default:
SSH_LOG(SSH_LOG_WARN, "Unsupported key type for security key signing");
return NULL;
}
/* Validate security key specific fields */
if (key->sk_key_handle == NULL) {
SSH_LOG(SSH_LOG_WARN, "Security key handle cannot be NULL");
return NULL;
}
if (key->sk_application == NULL ||
ssh_string_len(key->sk_application) == 0) {
SSH_LOG(SSH_LOG_WARN,
"Security key application cannot be NULL or empty");
return NULL;
}
if (context->sk_pin_callback != NULL) {
rc = context->sk_pin_callback(DEFAULT_PIN_PROMPT,
pin_buf,
sizeof(pin_buf),
0,
0,
context->sk_userdata);
if (rc == SSH_OK) {
pin_to_use = pin_buf;
} else {
SSH_LOG(SSH_LOG_WARN, "Failed to fetch PIN from callback");
explicit_bzero(pin_buf, sizeof(pin_buf));
goto error;
}
} else {
SSH_LOG(SSH_LOG_INFO, "Trying operation without PIN");
}
rc = sk_callbacks->sign(algorithm,
data,
data_len,
ssh_string_get_char(key->sk_application),
ssh_string_data(key->sk_key_handle),
ssh_string_len(key->sk_key_handle),
key->sk_flags,
pin_to_use,
context->sk_callbacks_options,
&sign_response);
explicit_bzero(pin_buf, sizeof(pin_buf));
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN,
"Security key sign callback failed: %s (%d)",
ssh_sk_err_to_string(rc),
rc);
goto error;
}
/* Convert SK sign response to ssh_signature */
rc = pki_sk_sign_response_to_ssh_signature(algorithm,
key->type,
sign_response,
&signature);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to convert sign response to signature");
goto error;
}
SK_SIGN_RESPONSE_FREE(sign_response);
return signature;
error:
SK_SIGN_RESPONSE_FREE(sign_response);
SSH_SIGNATURE_FREE(signature);
return NULL;
}
/**
* @brief Load resident keys from FIDO2 security keys
*
* This function loads all resident keys (discoverable credentials) stored
* on FIDO2 security keys using the context's security key callbacks.
* Resident keys are credentials stored directly on the security key device
* and can be discovered without prior knowledge of key handles.
*
* Only resident keys with SSH application identifiers (starting with
* "ssh:") are returned.
*
* @param[in] pki_context The PKI context containing security key callbacks.
* Can be NULL, in which case a default context with
* default callbacks will be used. If provided, the context
* must have valid sk_callbacks configured.
* @param[out] resident_keys_result Array of ssh_key structs representing the
* resident keys found and loaded
* @param[out] num_keys_found_result Number of resident keys found and loaded
*
* @return SSH_OK on success, SSH_ERROR on error
*
* @note The resident_keys_result array and its contents must be freed by
* the caller using ssh_sk_resident_key_free() for each key and then
* freeing the array itself when no longer needed.
*/
int ssh_sk_resident_keys_load(const struct ssh_pki_ctx_struct *pki_context,
ssh_key **resident_keys_result,
size_t *num_keys_found_result)
{
const struct ssh_sk_callbacks_struct *sk_callbacks = NULL;
struct sk_resident_key **raw_resident_keys = NULL;
ssh_key cur_resident_key = NULL, *result_keys = NULL, *temp_keys = NULL;
ssh_pki_ctx temp_ctx = NULL;
const struct ssh_pki_ctx_struct *ctx_to_use = NULL;
size_t raw_keys_count = 0, result_keys_count = 0, i;
uint8_t sk_flags;
char pin_buf[PIN_BUF_SIZE] = {0};
const char *pin_to_use = NULL;
int rc = SSH_ERROR;
/* If no context provided, create a temporary default one */
if (pki_context == NULL) {
SSH_LOG(SSH_LOG_INFO, "No PKI context provided, using the default one");
temp_ctx = ssh_pki_ctx_new();
if (temp_ctx == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create temporary PKI context");
return SSH_ERROR;
}
ctx_to_use = temp_ctx;
} else {
ctx_to_use = pki_context;
}
/* Get security key callbacks from context */
sk_callbacks = ctx_to_use->sk_callbacks;
if (sk_callbacks == NULL) {
SSH_LOG(SSH_LOG_WARN, "Security key callbacks cannot be NULL");
goto out;
}
if (!ssh_callbacks_exists(sk_callbacks, load_resident_keys)) {
SSH_LOG(SSH_LOG_WARN,
"Security key load resident keys callback is not implemented");
goto out;
}
if (resident_keys_result == NULL || num_keys_found_result == NULL) {
SSH_LOG(SSH_LOG_WARN, "Result pointers cannot be NULL");
goto out;
}
/* Initialize output parameters */
*resident_keys_result = NULL;
*num_keys_found_result = 0;
if (ctx_to_use->sk_pin_callback != NULL) {
rc = ctx_to_use->sk_pin_callback(DEFAULT_PIN_PROMPT,
pin_buf,
sizeof(pin_buf),
0,
0,
ctx_to_use->sk_userdata);
if (rc == SSH_OK) {
pin_to_use = pin_buf;
} else {
SSH_LOG(SSH_LOG_WARN, "Failed to fetch PIN from callback");
explicit_bzero(pin_buf, sizeof(pin_buf));
goto out;
}
} else {
SSH_LOG(SSH_LOG_INFO, "Trying operation without PIN");
}
rc = sk_callbacks->load_resident_keys(pin_to_use,
ctx_to_use->sk_callbacks_options,
&raw_resident_keys,
&raw_keys_count);
explicit_bzero(pin_buf, sizeof(pin_buf));
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN,
"Security key load_resident_keys callback failed: %s (%d)",
ssh_sk_err_to_string(rc),
rc);
goto out;
}
/* Process each raw resident key */
for (i = 0; i < raw_keys_count; i++) {
SSH_LOG(
SSH_LOG_DEBUG,
"Processing resident key %zu: alg %d, app \"%s\", user_id_len %zu",
i,
raw_resident_keys[i]->alg,
raw_resident_keys[i]->application,
raw_resident_keys[i]->user_id_len);
/* Filter out non-SSH applications */
if (strncmp(raw_resident_keys[i]->application, "ssh:", 4) != 0) {
SSH_LOG(SSH_LOG_DEBUG,
"Skipping non-SSH application: %s",
raw_resident_keys[i]->application);
continue;
}
/* Check supported algorithms */
switch (raw_resident_keys[i]->alg) {
#ifdef HAVE_ECC
case SSH_SK_ECDSA:
break;
#endif /* HAVE_ECC */
case SSH_SK_ED25519:
break;
default:
SSH_LOG(SSH_LOG_WARN,
"Unsupported algorithm %d, skipping",
raw_resident_keys[i]->alg);
continue;
}
/* Set up security key flags */
sk_flags = SSH_SK_USER_PRESENCE_REQD | SSH_SK_RESIDENT_KEY;
if (raw_resident_keys[i]->flags & SSH_SK_USER_VERIFICATION_REQD) {
sk_flags |= SSH_SK_USER_VERIFICATION_REQD;
}
/* Convert raw resident key to libssh key structure */
rc =
pki_sk_enroll_response_to_ssh_key(raw_resident_keys[i]->alg,
raw_resident_keys[i]->application,
&raw_resident_keys[i]->key,
&cur_resident_key);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to convert resident key %zu to ssh_key",
i);
continue;
}
/* Set the security key flags on the converted key */
cur_resident_key->sk_flags = sk_flags;
/* Copy user ID if present */
if (raw_resident_keys[i]->user_id != NULL &&
raw_resident_keys[i]->user_id_len > 0) {
cur_resident_key->sk_user_id =
ssh_string_from_data(raw_resident_keys[i]->user_id,
raw_resident_keys[i]->user_id_len);
if (cur_resident_key->sk_user_id == NULL) {
SSH_LOG(SSH_LOG_WARN,
"Failed to allocate user_id string for key %zu",
i);
goto out;
}
}
/* Grow the result array */
temp_keys =
realloc(result_keys, sizeof(ssh_key) * (result_keys_count + 1));
if (temp_keys == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to reallocate result keys array");
goto out;
}
/* Add the current resident key to the result array */
result_keys = temp_keys;
result_keys[result_keys_count] = cur_resident_key;
result_keys_count++;
cur_resident_key = NULL;
}
/* Set output parameters */
*resident_keys_result = result_keys;
*num_keys_found_result = result_keys_count;
result_keys = NULL;
result_keys_count = 0;
rc = SSH_OK;
out:
if (raw_resident_keys != NULL) {
for (i = 0; i < raw_keys_count; i++) {
SK_RESIDENT_KEY_FREE(raw_resident_keys[i]);
}
SAFE_FREE(raw_resident_keys);
}
SSH_KEY_FREE(cur_resident_key);
for (i = 0; i < result_keys_count; i++) {
SSH_KEY_FREE(result_keys[i]);
}
SAFE_FREE(result_keys);
/* Clean up temporary context if we created one */
if (temp_ctx != NULL) {
SSH_PKI_CTX_FREE(temp_ctx);
}
return rc;
}
/** @} */