Coverage Report

Created: 2026-02-05 06:49

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libssh/src/pki_context.c
Line
Count
Source
1
/*
2
 * This file is part of the SSH Library
3
 *
4
 * Copyright (c) 2025 Praneeth Sarode <praneethsarode@gmail.com>
5
 *
6
 * The SSH Library is free software; you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published by
8
 * the Free Software Foundation, version 2.1 of the License.
9
 *
10
 * The SSH Library is distributed in the hope that it will be useful, but
11
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
13
 * License for more details.
14
 *
15
 * You should have received a copy of the GNU Lesser General Public License
16
 * along with the SSH Library; see the file COPYING.  If not, write to
17
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
18
 * MA 02111-1307, USA.
19
 */
20
21
#include "config.h"
22
23
#include "libssh/libssh.h"
24
#include "libssh/pki.h"
25
#include "libssh/pki_context.h"
26
#include "libssh/priv.h"
27
#include "libssh/sk_common.h"
28
29
#ifdef WITH_FIDO2
30
#include "libssh/buffer.h"
31
#include "libssh/callbacks.h"
32
#include "libssh/sk_api.h"
33
#endif /* WITH_FIDO2 */
34
35
/**
36
 * @addtogroup libssh_pki
37
 * @{
38
 */
39
40
/**
41
 * @brief Allocate a new generic PKI context container.
42
 *
43
 * Allocates and default-initializes a new ssh_pki_ctx instance.
44
 *
45
 * @return Newly allocated context on success, or NULL on allocation failure.
46
 * @see ssh_pki_ctx_free()
47
 */
48
ssh_pki_ctx ssh_pki_ctx_new(void)
49
13.2k
{
50
13.2k
    struct ssh_pki_ctx_struct *ctx = NULL;
51
52
13.2k
    ctx = calloc(1, sizeof(struct ssh_pki_ctx_struct));
53
13.2k
    if (ctx == NULL) {
54
0
        return NULL;
55
0
    }
56
57
#ifdef WITH_FIDO2
58
    /* Initialize SK fields with default, if available. */
59
    ctx->sk_callbacks = ssh_sk_get_default_callbacks();
60
61
    /*
62
     * Both OpenSSH security key enrollment and server authentication require
63
     * user presence by default, so we replicate that for consistency.
64
     */
65
    ctx->sk_flags = SSH_SK_USER_PRESENCE_REQD;
66
67
    ctx->sk_application = strdup("ssh:");
68
    if (ctx->sk_application == NULL) {
69
        SSH_LOG(SSH_LOG_WARN,
70
                "Failed to allocate memory for default application");
71
        SAFE_FREE(ctx);
72
        return NULL;
73
    }
74
#endif /* WITH_FIDO2 */
75
76
13.2k
    return ctx;
77
13.2k
}
78
79
/**
80
 * @brief Free a generic PKI context container.
81
 *
82
 * @param[in] context  The PKI context to free (may be NULL).
83
 * @see ssh_pki_ctx_new()
84
 */
85
void ssh_pki_ctx_free(ssh_pki_ctx context)
86
13.2k
{
87
13.2k
    if (context == NULL) {
88
0
        return;
89
0
    }
90
91
#ifdef WITH_FIDO2
92
    SAFE_FREE(context->sk_application);
93
    SSH_BUFFER_FREE(context->sk_challenge_buffer);
94
    SSH_BUFFER_FREE(context->sk_attestation_buffer);
95
    SK_OPTIONS_FREE(context->sk_callbacks_options);
96
#endif /* WITH_FIDO2 */
97
98
13.2k
    SAFE_FREE(context);
99
13.2k
}
100
101
/**
102
 * @brief Set various options for a PKI context.
103
 *
104
 * This function can set all possible PKI context options.
105
 *
106
 * @param[in] context  Target PKI context.
107
 * @param option  The option type to set. This could be one of the following:
108
 *
109
 *                - SSH_PKI_OPTION_RSA_KEY_SIZE (int):
110
 *                  Set the RSA key size in bits for key generation.
111
 *                  Typically 2048, 3072, or 4096 bits. Must be greater
112
 *                  than or equal to 1024, as anything below is considered
113
 *                  insecure.
114
 *
115
 *                - SSH_PKI_OPTION_SK_APPLICATION (const char *):
116
 *                  The Relying Party identifier (application string) that
117
 *                  determines which service/domain this security key
118
 *                  credential will be associated with. This is a required
119
 *                  field for all security key generation operations.
120
 *                  The application string typically starts with "ssh:" for
121
 *                  SSH keys. It is copied internally and can be freed
122
 *                  after setting.
123
 *
124
 *                - SSH_PKI_SK_OPTION_FLAGS (uint8_t):
125
 *                  Set FIDO2/U2F operation flags that control how the FIDO2/U2F
126
 *                  authenticator behaves during generation operations. Multiple
127
 *                  flags can be combined using bitwise OR operations. The
128
 *                  pointer must not be NULL.
129
 *
130
 *                  Available flags:
131
 *
132
 *                  SSH_SK_USER_PRESENCE_REQD: Requires user presence
133
 *
134
 *                  SSH_SK_USER_VERIFICATION_REQD: Requires user verification
135
 *
136
 *                  SSH_SK_FORCE_OPERATION: Forces generation even if a
137
 *                  resident key already exists.
138
 *
139
 *                  SSH_SK_RESIDENT_KEY: Creates a resident
140
 *                  key stored on the authenticator.
141
 *
142
 *                - SSH_PKI_OPTION_SK_USER_ID (const char *):
143
 *                  Sets the user identifier to associate with a resident
144
 *                  credential during enrollment. When a resident key is
145
 *                  requested (SSH_SK_RESIDENT_KEY), this ID is stored on the
146
 *                  authenticator and later used to look up or prevent duplicate
147
 *                  credentials. Maximum length is SK_MAX_USER_ID_LEN bytes;
148
 *                  longer values will cause the operation to fail.
149
 *
150
 *                - SSH_PKI_OPTION_SK_CHALLENGE (ssh_buffer):
151
 *                  Set custom cryptographic challenge data to be included in
152
 *                  the generation operation. The challenge is signed by the
153
 *                  authenticator during key generation. If not provided,
154
 *                  a random 32-byte challenge will be automatically generated.
155
 *                  The challenge data is copied internally and the caller
156
 *                  retains ownership of the provided buffer.
157
 *
158
 *                - SSH_PKI_OPTION_SK_CALLBACKS (ssh_sk_callbacks):
159
 *                  Set the security key callback structure to use custom
160
 *                  callback functions for FIDO2/U2F operations like enrollment,
161
 *                  signing, and loading resident keys. The structure is not
162
 *                  copied so it needs to be valid for the whole context
163
 *                  lifetime or until replaced.
164
 *
165
 * @param value     The value to set. This is a generic pointer and the
166
 *                  datatype which is used should be set according to the
167
 *                  option type.
168
 *
169
 * @return          SSH_OK on success, SSH_ERROR on error.
170
 *
171
 * @warning         When the option value to set is represented via a pointer
172
 *                  (e.g const char *, ssh_buffer), the value parameter
173
 *                  should be that pointer. Do NOT pass a pointer to a
174
 *                  pointer.
175
 *
176
 * @warning         When the option value to set is not a pointer (e.g int,
177
 *                  uint8_t), the value parameter should be a pointer to the
178
 *                  location storing the value to set (int *, uint8_t *).
179
 */
180
int ssh_pki_ctx_options_set(ssh_pki_ctx context,
181
                            enum ssh_pki_options_e option,
182
                            const void *value)
183
0
{
184
0
    if (context == NULL) {
185
0
        SSH_LOG(SSH_LOG_WARN, "Invalid PKI context passed");
186
0
        return SSH_ERROR;
187
0
    }
188
189
0
    switch (option) {
190
0
    case SSH_PKI_OPTION_RSA_KEY_SIZE:
191
0
        if (value == NULL) {
192
0
            SSH_LOG(SSH_LOG_WARN, "RSA key size pointer must not be NULL");
193
0
            return SSH_ERROR;
194
0
        } else if (*(int *)value != 0 && *(int *)value <= RSA_MIN_KEY_SIZE) {
195
0
            SSH_LOG(
196
0
                SSH_LOG_WARN,
197
0
                "RSA key size must be greater than %d bits or 0 for default",
198
0
                RSA_MIN_KEY_SIZE);
199
0
            return SSH_ERROR;
200
0
        }
201
0
        context->rsa_key_size = *(int *)value;
202
0
        break;
203
204
#ifdef WITH_FIDO2
205
    case SSH_PKI_OPTION_SK_APPLICATION:
206
        SAFE_FREE(context->sk_application);
207
        if (value != NULL) {
208
            context->sk_application = strdup((char *)value);
209
            if (context->sk_application == NULL) {
210
                SSH_LOG(SSH_LOG_WARN,
211
                        "Failed to allocate memory for application");
212
                return SSH_ERROR;
213
            }
214
        }
215
        break;
216
217
    case SSH_PKI_OPTION_SK_FLAGS:
218
        if (value == NULL) {
219
            return SSH_ERROR;
220
        } else {
221
            context->sk_flags = *(uint8_t *)value;
222
        }
223
        break;
224
225
    case SSH_PKI_OPTION_SK_USER_ID: {
226
        int rc;
227
228
        /*
229
         * Set required to false, because only the enrollment callback supports
230
         * the user ID option, and if this context is used for any other
231
         * operation, it would fail unnecessarily.
232
         */
233
        rc = ssh_pki_ctx_sk_callbacks_option_set(context,
234
                                                 SSH_SK_OPTION_NAME_USER_ID,
235
                                                 value,
236
                                                 false);
237
        if (rc != SSH_OK) {
238
            return SSH_ERROR;
239
        }
240
        break;
241
    }
242
243
    case SSH_PKI_OPTION_SK_CHALLENGE: {
244
        SSH_BUFFER_FREE(context->sk_challenge_buffer);
245
        if (value == NULL) {
246
            break;
247
        }
248
249
        context->sk_challenge_buffer = ssh_buffer_dup((ssh_buffer)value);
250
        if (context->sk_challenge_buffer == NULL) {
251
            SSH_LOG(SSH_LOG_WARN, "Failed to duplicate challenge buffer");
252
            return SSH_ERROR;
253
        }
254
        ssh_buffer_set_secure(context->sk_challenge_buffer);
255
        break;
256
    }
257
258
    case SSH_PKI_OPTION_SK_CALLBACKS: {
259
        bool is_compatible = sk_callbacks_check_compatibility(value);
260
        if (!is_compatible) {
261
            return SSH_ERROR;
262
        }
263
        context->sk_callbacks = value;
264
        break;
265
    }
266
#else  /* WITH_FIDO2 */
267
0
    case SSH_PKI_OPTION_SK_APPLICATION:
268
0
    case SSH_PKI_OPTION_SK_FLAGS:
269
0
    case SSH_PKI_OPTION_SK_USER_ID:
270
0
    case SSH_PKI_OPTION_SK_CHALLENGE:
271
0
    case SSH_PKI_OPTION_SK_CALLBACKS:
272
0
        SSH_LOG(SSH_LOG_WARN, SK_NOT_SUPPORTED_MSG);
273
0
        return SSH_ERROR;
274
0
#endif /* WITH_FIDO2 */
275
276
0
    default:
277
0
        SSH_LOG(SSH_LOG_WARN, "Unknown PKI context option: %d", option);
278
0
        return SSH_ERROR;
279
0
    }
280
281
0
    return SSH_OK;
282
0
}
283
284
/**
285
 * @brief Set the PIN callback function to get the PIN for security
286
 * key authenticator access.
287
 *
288
 * @param context      The PKI context to modify.
289
 * @param pin_callback The callback used when the authenticator requires PIN
290
 *                     entry for verification.
291
 * @param userdata     A generic pointer that is passed as the userdata
292
 *                     argument to the callback function. Can be NULL.
293
 *
294
 * @return SSH_OK on success, SSH_ERROR if context is NULL.
295
 *
296
 * @note The callback and userdata are stored internally in the context
297
 *       structure and must remain valid until the context is freed or
298
 *       replaced.
299
 *
300
 * @see ssh_auth_callback
301
 */
302
int ssh_pki_ctx_set_sk_pin_callback(ssh_pki_ctx context,
303
                                    ssh_auth_callback pin_callback,
304
                                    void *userdata)
305
0
{
306
#ifdef WITH_FIDO2
307
    if (context == NULL) {
308
        SSH_LOG(SSH_LOG_WARN, "Context should not be NULL");
309
        return SSH_ERROR;
310
    }
311
312
    context->sk_pin_callback = pin_callback;
313
    context->sk_userdata = userdata;
314
315
    return SSH_OK;
316
317
#else
318
0
    (void)context;
319
0
    (void)pin_callback;
320
0
    (void)userdata;
321
322
0
    SSH_LOG(SSH_LOG_WARN, SK_NOT_SUPPORTED_MSG);
323
0
    return SSH_ERROR;
324
0
#endif /* WITH_FIDO2 */
325
0
}
326
327
/**
328
 * @brief Set a security key (FIDO2/U2F) callback option in the
329
 * context. These options are passed to the sk_callbacks during
330
 * enroll/sign/load_resident_keys operations.
331
 *
332
 * Both the name and value strings are duplicated internally so the caller
333
 * retains ownership of the original pointers.
334
 *
335
 * @param[in] context   The PKI context. Must not be NULL.
336
 * @param[in] name      option name string. Must not be NULL.
337
 * @param[in] value     option value string. Must not be NULL.
338
 * @param[in] required  Set to true if the option is mandatory. If set and the
339
 *                      ssh_sk_callbacks do not recognize the option,
340
 *                      the operation should fail.
341
 *
342
 * @return SSH_OK on success, SSH_ERROR on allocation failure or invalid args.
343
 *
344
 * @note The option objects are freed automatically when the context is freed
345
 *       via ssh_pki_sk_ctx_free().
346
 *
347
 * @see ssh_sk_callbacks_struct
348
 */
349
int ssh_pki_ctx_sk_callbacks_option_set(ssh_pki_ctx context,
350
                                        const char *name,
351
                                        const char *value,
352
                                        bool required)
353
0
{
354
#ifdef WITH_FIDO2
355
    struct sk_option *new_option = NULL;
356
    struct sk_option **temp = NULL;
357
    size_t count = 0;
358
359
    if (context == NULL || name == NULL || value == NULL) {
360
        SSH_LOG(SSH_LOG_WARN, "Invalid parameters passed");
361
        return SSH_ERROR;
362
    }
363
364
    /* Count existing options */
365
    if (context->sk_callbacks_options != NULL) {
366
        while (context->sk_callbacks_options[count] != NULL) {
367
            count++;
368
        }
369
    }
370
371
    /* Allocate new option */
372
    new_option = calloc(1, sizeof(struct sk_option));
373
    if (new_option == NULL) {
374
        SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for new option");
375
        return SSH_ERROR;
376
    }
377
378
    new_option->name = strdup(name);
379
    if (new_option->name == NULL) {
380
        SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for option name");
381
        SAFE_FREE(new_option);
382
        return SSH_ERROR;
383
    }
384
385
    new_option->value = strdup(value);
386
    if (new_option->value == NULL) {
387
        SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for option value");
388
        SAFE_FREE(new_option->name);
389
        SAFE_FREE(new_option);
390
        return SSH_ERROR;
391
    }
392
393
    new_option->required = required;
394
395
    /* Reallocate array to accommodate new option */
396
    temp = realloc(context->sk_callbacks_options,
397
                   (count + 2) * sizeof(struct sk_option *));
398
    if (temp == NULL) {
399
        SSH_LOG(SSH_LOG_WARN, "Failed to reallocate options array");
400
        SAFE_FREE(new_option->name);
401
        SAFE_FREE(new_option->value);
402
        SAFE_FREE(new_option);
403
        return SSH_ERROR;
404
    }
405
406
    context->sk_callbacks_options = temp;
407
    context->sk_callbacks_options[count] = new_option;
408
    context->sk_callbacks_options[count + 1] = NULL;
409
410
    return SSH_OK;
411
#else
412
0
    (void)context;
413
0
    (void)name;
414
0
    (void)value;
415
0
    (void)required;
416
417
0
    SSH_LOG(SSH_LOG_WARN, SK_NOT_SUPPORTED_MSG);
418
0
    return SSH_ERROR;
419
0
#endif /* WITH_FIDO2 */
420
0
}
421
422
/**
423
 * @brief Clear all sk_callbacks options.
424
 *
425
 * Removes and frees all previously set sk_callbacks options from the context.
426
 *
427
 * @param[in] context The PKI context to modify.
428
 *
429
 * @return SSH_OK on success, SSH_ERROR if context is NULL.
430
 */
431
int ssh_pki_ctx_sk_callbacks_options_clear(ssh_pki_ctx context)
432
0
{
433
#ifdef WITH_FIDO2
434
    if (context == NULL) {
435
        SSH_LOG(SSH_LOG_WARN, "Context should not be NULL");
436
        return SSH_ERROR;
437
    }
438
439
    SK_OPTIONS_FREE(context->sk_callbacks_options);
440
    return SSH_OK;
441
#else
442
0
    (void)context;
443
444
0
    SSH_LOG(SSH_LOG_WARN, SK_NOT_SUPPORTED_MSG);
445
0
    return SSH_ERROR;
446
0
#endif /* WITH_FIDO2 */
447
0
}
448
449
/**
450
 * @brief Get a copy of the attestation buffer from a PKI context.
451
 *
452
 * Retrieves a copy of the attestation buffer stored in the context after a key
453
 * enrollment operation. The attestation buffer contains serialized attestation
454
 * information in the "ssh-sk-attest-v01" format.
455
 *
456
 * @param[in] context The PKI context. Must not be NULL.
457
 * @param[out] attestation_buffer Pointer to store a copy of the attestation
458
 *                                buffer. Will be set to NULL if no attestation
459
 *                                data is available (e.g., authenticator doesn't
460
 *                                support attestation, or attestation data
461
 *                                was invalid/incomplete).
462
 *
463
 * @return SSH_OK on success, SSH_ERROR if context or attestation_buffer is
464
 *         NULL, or if buffer duplication fails.
465
 *
466
 * @note The caller is responsible for freeing the returned buffer using
467
 *       SSH_BUFFER_FREE().
468
 */
469
int ssh_pki_ctx_get_sk_attestation_buffer(
470
    const struct ssh_pki_ctx_struct *context,
471
    ssh_buffer *attestation_buffer)
472
0
{
473
#ifdef WITH_FIDO2
474
    if (context == NULL) {
475
        SSH_LOG(SSH_LOG_WARN, "Context should not be NULL");
476
        return SSH_ERROR;
477
    }
478
479
    if (attestation_buffer == NULL) {
480
        SSH_LOG(SSH_LOG_WARN, "attestation_buffer pointer should not be NULL");
481
        return SSH_ERROR;
482
    }
483
484
    *attestation_buffer = ssh_buffer_dup(context->sk_attestation_buffer);
485
    if (*attestation_buffer == NULL) {
486
        SSH_LOG(SSH_LOG_WARN, "Failed to duplicate attestation buffer");
487
        return SSH_ERROR;
488
    }
489
490
    return SSH_OK;
491
#else
492
0
    (void)context;
493
0
    (void)attestation_buffer;
494
495
0
    SSH_LOG(SSH_LOG_WARN, SK_NOT_SUPPORTED_MSG);
496
0
    return SSH_ERROR;
497
0
#endif /* WITH_FIDO2 */
498
0
}
499
500
/**
501
 * @brief Duplicate an existing PKI context
502
 *
503
 * Creates a new PKI context and copies all fields from the source context.
504
 * This function performs deep copying for all dynamically allocated fields
505
 * to ensure independent ownership between source and destination contexts.
506
 *
507
 * @param[in] context  The PKI context to copy from
508
 *
509
 * @return             New PKI context with copied data on success,
510
 *                     NULL on failure or if src_context is NULL
511
 */
512
ssh_pki_ctx ssh_pki_ctx_dup(const ssh_pki_ctx context)
513
0
{
514
0
    ssh_pki_ctx new_context = NULL;
515
516
0
    if (context == NULL) {
517
0
        return NULL;
518
0
    }
519
520
0
    new_context = ssh_pki_ctx_new();
521
0
    if (new_context == NULL) {
522
0
        goto error;
523
0
    }
524
525
0
    new_context->rsa_key_size = context->rsa_key_size;
526
527
#ifdef WITH_FIDO2
528
    new_context->sk_callbacks = context->sk_callbacks;
529
530
    // Free the default application string before copying
531
    SAFE_FREE(new_context->sk_application);
532
533
    if (context->sk_application != NULL) {
534
        new_context->sk_application = strdup(context->sk_application);
535
        if (new_context->sk_application == NULL) {
536
            SSH_LOG(SSH_LOG_WARN, "Failed to copy SK application string");
537
            goto error;
538
        }
539
    }
540
541
    new_context->sk_flags = context->sk_flags;
542
543
    new_context->sk_pin_callback = context->sk_pin_callback;
544
    new_context->sk_userdata = context->sk_userdata;
545
546
    if (context->sk_challenge_buffer != NULL) {
547
        new_context->sk_challenge_buffer =
548
            ssh_buffer_dup(context->sk_challenge_buffer);
549
        if (new_context->sk_challenge_buffer == NULL) {
550
            SSH_LOG(SSH_LOG_WARN, "Failed to copy SK challenge buffer");
551
            goto error;
552
        }
553
    }
554
555
    if (context->sk_callbacks_options != NULL) {
556
        new_context->sk_callbacks_options = sk_options_dup(
557
            (const struct sk_option **)context->sk_callbacks_options);
558
        if (new_context->sk_callbacks_options == NULL) {
559
            SSH_LOG(SSH_LOG_WARN, "Failed to copy SK callbacks options");
560
            goto error;
561
        }
562
    }
563
564
    if (context->sk_attestation_buffer != NULL) {
565
        new_context->sk_attestation_buffer =
566
            ssh_buffer_dup(context->sk_attestation_buffer);
567
        if (new_context->sk_attestation_buffer == NULL) {
568
            SSH_LOG(SSH_LOG_WARN, "Failed to copy SK attestation buffer");
569
            goto error;
570
        }
571
    }
572
#endif /* WITH_FIDO2 */
573
574
0
    return new_context;
575
576
0
error:
577
0
    SSH_PKI_CTX_FREE(new_context);
578
    return NULL;
579
0
}
580
581
/** @} */