Coverage Report

Created: 2026-08-13 07:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/openssh/ssh-ecdsa-sk.c
Line
Count
Source
1
/* $OpenBSD: ssh-ecdsa-sk.c,v 1.21 2026/02/06 22:59:18 dtucker Exp $ */
2
/*
3
 * Copyright (c) 2000 Markus Friedl.  All rights reserved.
4
 * Copyright (c) 2010 Damien Miller.  All rights reserved.
5
 * Copyright (c) 2019 Google Inc.  All rights reserved.
6
 *
7
 * Redistribution and use in source and binary forms, with or without
8
 * modification, are permitted provided that the following conditions
9
 * are met:
10
 * 1. Redistributions of source code must retain the above copyright
11
 *    notice, this list of conditions and the following disclaimer.
12
 * 2. Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
17
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
20
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
25
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
/* #define DEBUG_SK 1 */
29
30
#include "includes.h"
31
32
#include <sys/types.h>
33
34
#ifdef WITH_OPENSSL
35
#include "openbsd-compat/openssl-compat.h"
36
#include <openssl/bn.h>
37
#include <openssl/ec.h>
38
#include <openssl/ecdsa.h>
39
#include <openssl/evp.h>
40
41
#include <string.h>
42
#include <stdio.h> /* needed for DEBUG_SK only */
43
44
#include "sshbuf.h"
45
#include "ssherr.h"
46
#include "digest.h"
47
#define SSHKEY_INTERNAL
48
#include "sshkey.h"
49
50
/* Reuse some ECDSA internals */
51
extern struct sshkey_impl_funcs sshkey_ecdsa_funcs;
52
53
static void
54
ssh_ecdsa_sk_cleanup(struct sshkey *k)
55
0
{
56
0
  sshkey_sk_cleanup(k);
57
0
  sshkey_ecdsa_funcs.cleanup(k);
58
0
}
59
60
static int
61
ssh_ecdsa_sk_equal(const struct sshkey *a, const struct sshkey *b)
62
0
{
63
0
  if (!sshkey_sk_fields_equal(a, b))
64
0
    return 0;
65
0
  if (!sshkey_ecdsa_funcs.equal(a, b))
66
0
    return 0;
67
0
  return 1;
68
0
}
69
70
static int
71
ssh_ecdsa_sk_serialize_public(const struct sshkey *key, struct sshbuf *b,
72
    enum sshkey_serialize_rep opts)
73
0
{
74
0
  int r;
75
76
0
  if ((r = sshkey_ecdsa_funcs.serialize_public(key, b, opts)) != 0)
77
0
    return r;
78
0
  if ((r = sshkey_serialize_sk(key, b)) != 0)
79
0
    return r;
80
81
0
  return 0;
82
0
}
83
84
static int
85
ssh_ecdsa_sk_serialize_private(const struct sshkey *key, struct sshbuf *b,
86
    enum sshkey_serialize_rep opts)
87
0
{
88
0
  int r;
89
90
0
  if (!sshkey_is_cert(key)) {
91
0
    if ((r = sshkey_ecdsa_funcs.serialize_public(key,
92
0
        b, opts)) != 0)
93
0
      return r;
94
0
  }
95
0
  if ((r = sshkey_serialize_private_sk(key, b)) != 0)
96
0
    return r;
97
98
0
  return 0;
99
0
}
100
101
static int
102
ssh_ecdsa_sk_copy_public(const struct sshkey *from, struct sshkey *to)
103
0
{
104
0
  int r;
105
106
0
  if ((r = sshkey_ecdsa_funcs.copy_public(from, to)) != 0)
107
0
    return r;
108
0
  if ((r = sshkey_copy_public_sk(from, to)) != 0)
109
0
    return r;
110
0
  return 0;
111
0
}
112
113
static int
114
ssh_ecdsa_sk_deserialize_public(const char *ktype, struct sshbuf *b,
115
    struct sshkey *key)
116
0
{
117
0
  int r;
118
119
0
  if ((r = sshkey_ecdsa_funcs.deserialize_public(ktype, b, key)) != 0)
120
0
    return r;
121
0
  if ((r = sshkey_deserialize_sk(b, key)) != 0)
122
0
    return r;
123
0
  return 0;
124
0
}
125
126
static int
127
ssh_ecdsa_sk_deserialize_private(const char *ktype, struct sshbuf *b,
128
    struct sshkey *key)
129
0
{
130
0
  int r;
131
132
0
  if (!sshkey_is_cert(key)) {
133
0
    if ((r = sshkey_ecdsa_funcs.deserialize_public(ktype,
134
0
        b, key)) != 0)
135
0
      return r;
136
0
  }
137
0
  if ((r = sshkey_private_deserialize_sk(b, key)) != 0)
138
0
    return r;
139
140
0
  return 0;
141
0
}
142
143
/*
144
 * Check FIDO/W3C webauthn signatures clientData field against the expected
145
 * format and prepare a hash of it for use in signature verification.
146
 *
147
 * webauthn signatures do not sign the hash of the message directly, but
148
 * instead sign a JSON-like "clientData" wrapper structure that contains the
149
 * message hash along with a other information.
150
 *
151
 * Fortunately this structure has a fixed format so it is possible to verify
152
 * that the hash of the signed message is present within the clientData
153
 * structure without needing to implement any JSON parsing.
154
 */
155
static int
156
webauthn_check_prepare_hash(const u_char *data, size_t datalen,
157
    const char *origin, const struct sshbuf *wrapper,
158
    uint8_t flags, const struct sshbuf *extensions,
159
    u_char *msghash, size_t msghashlen)
160
0
{
161
0
  int r = SSH_ERR_INTERNAL_ERROR;
162
0
  struct sshbuf *chall = NULL, *m = NULL;
163
164
0
  if ((m = sshbuf_new()) == NULL ||
165
0
      (chall = sshbuf_from(data, datalen)) == NULL) {
166
0
    r = SSH_ERR_ALLOC_FAIL;
167
0
    goto out;
168
0
  }
169
  /*
170
   * Ensure origin contains no quote character and that the flags are
171
   * consistent with what we received
172
   */
173
0
  if (strchr(origin, '\"') != NULL ||
174
0
      (flags & 0x40) != 0 /* AD */ ||
175
0
      ((flags & 0x80) == 0 /* ED */) != (sshbuf_len(extensions) == 0)) {
176
0
    r = SSH_ERR_INVALID_FORMAT;
177
0
    goto out;
178
0
  }
179
180
  /*
181
   * Prepare the preamble to clientData that we expect, poking the
182
   * challenge and origin into their canonical positions in the
183
   * structure. The crossOrigin flag and any additional extension
184
   * fields present are ignored.
185
   */
186
0
#define WEBAUTHN_0  "{\"type\":\"webauthn.get\",\"challenge\":\""
187
0
#define WEBAUTHN_1  "\",\"origin\":\""
188
0
#define WEBAUTHN_2  "\""
189
0
  if ((r = sshbuf_put(m, WEBAUTHN_0, sizeof(WEBAUTHN_0) - 1)) != 0 ||
190
0
      (r = sshbuf_dtourlb64(chall, m, 0)) != 0 ||
191
0
      (r = sshbuf_put(m, WEBAUTHN_1, sizeof(WEBAUTHN_1) - 1)) != 0 ||
192
0
      (r = sshbuf_put(m, origin, strlen(origin))) != 0 ||
193
0
      (r = sshbuf_put(m, WEBAUTHN_2, sizeof(WEBAUTHN_2) - 1)) != 0)
194
0
    goto out;
195
#ifdef DEBUG_SK
196
  fprintf(stderr, "%s: received origin: %s\n", __func__, origin);
197
  fprintf(stderr, "%s: received clientData:\n", __func__);
198
  sshbuf_dump(wrapper, stderr);
199
  fprintf(stderr, "%s: expected clientData preamble:\n", __func__);
200
  sshbuf_dump(m, stderr);
201
#endif
202
  /* Check that the supplied clientData has the preamble we expect */
203
0
  if ((r = sshbuf_cmp(wrapper, 0, sshbuf_ptr(m), sshbuf_len(m))) != 0)
204
0
    goto out;
205
206
  /* Prepare hash of clientData */
207
0
  if ((r = ssh_digest_buffer(SSH_DIGEST_SHA256, wrapper,
208
0
      msghash, msghashlen)) != 0)
209
0
    goto out;
210
211
  /* success */
212
0
  r = 0;
213
0
 out:
214
0
  sshbuf_free(chall);
215
0
  sshbuf_free(m);
216
0
  return r;
217
0
}
218
219
static int
220
ssh_ecdsa_sk_verify(const struct sshkey *key,
221
    const u_char *sig, size_t siglen,
222
    const u_char *data, size_t dlen, const char *alg, u_int compat,
223
    struct sshkey_sig_details **detailsp)
224
0
{
225
0
  ECDSA_SIG *esig = NULL;
226
0
  EVP_MD_CTX *md_ctx = NULL;
227
0
  BIGNUM *sig_r = NULL, *sig_s = NULL;
228
0
  u_char sig_flags;
229
0
  u_char msghash[32], apphash[32];
230
0
  u_int sig_counter;
231
0
  u_char *sigb = NULL, *cp;
232
0
  int is_webauthn = 0, ret = SSH_ERR_INTERNAL_ERROR, len = 0;
233
0
  struct sshbuf *b = NULL, *sigbuf = NULL, *original_signed = NULL;
234
0
  struct sshbuf *webauthn_wrapper = NULL, *webauthn_exts = NULL;
235
0
  char *ktype = NULL, *webauthn_origin = NULL;
236
0
  struct sshkey_sig_details *details = NULL;
237
#ifdef DEBUG_SK
238
  char *tmp = NULL;
239
#endif
240
241
0
  if (detailsp != NULL)
242
0
    *detailsp = NULL;
243
0
  if (key == NULL || key->pkey == NULL ||
244
0
      sshkey_type_plain(key->type) != KEY_ECDSA_SK ||
245
0
      sig == NULL || siglen == 0)
246
0
    return SSH_ERR_INVALID_ARGUMENT;
247
248
0
  if (key->ecdsa_nid != NID_X9_62_prime256v1)
249
0
    return SSH_ERR_INTERNAL_ERROR;
250
251
  /* fetch signature */
252
0
  if ((b = sshbuf_from(sig, siglen)) == NULL)
253
0
    return SSH_ERR_ALLOC_FAIL;
254
0
  if ((details = calloc(1, sizeof(*details))) == NULL) {
255
0
    ret = SSH_ERR_ALLOC_FAIL;
256
0
    goto out;
257
0
  }
258
0
  if (sshbuf_get_cstring(b, &ktype, NULL) != 0) {
259
0
    ret = SSH_ERR_INVALID_FORMAT;
260
0
    goto out;
261
0
  }
262
0
  if (strcmp(ktype, "webauthn-sk-ecdsa-sha2-nistp256@openssh.com") == 0 ||
263
0
      strcmp(ktype, "webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.com")
264
0
        == 0)
265
0
    is_webauthn = 1;
266
0
  else if (strcmp(ktype, "sk-ecdsa-sha2-nistp256@openssh.com") != 0) {
267
0
    ret = SSH_ERR_INVALID_FORMAT;
268
0
    goto out;
269
0
  }
270
0
  if (sshbuf_froms(b, &sigbuf) != 0 ||
271
0
      sshbuf_get_u8(b, &sig_flags) != 0 ||
272
0
      sshbuf_get_u32(b, &sig_counter) != 0) {
273
0
    ret = SSH_ERR_INVALID_FORMAT;
274
0
    goto out;
275
0
  }
276
0
  if (is_webauthn) {
277
0
    if (sshbuf_get_cstring(b, &webauthn_origin, NULL) != 0 ||
278
0
        sshbuf_froms(b, &webauthn_wrapper) != 0 ||
279
0
        sshbuf_froms(b, &webauthn_exts) != 0) {
280
0
      ret = SSH_ERR_INVALID_FORMAT;
281
0
      goto out;
282
0
    }
283
0
  }
284
0
  if (sshbuf_len(b) != 0) {
285
0
    ret = SSH_ERR_UNEXPECTED_TRAILING_DATA;
286
0
    goto out;
287
0
  }
288
289
  /* parse signature */
290
0
  if (sshbuf_get_bignum2(sigbuf, &sig_r) != 0 ||
291
0
      sshbuf_get_bignum2(sigbuf, &sig_s) != 0) {
292
0
    ret = SSH_ERR_INVALID_FORMAT;
293
0
    goto out;
294
0
  }
295
0
  if (sshbuf_len(sigbuf) != 0) {
296
0
    ret = SSH_ERR_UNEXPECTED_TRAILING_DATA;
297
0
    goto out;
298
0
  }
299
300
#ifdef DEBUG_SK
301
  fprintf(stderr, "%s: data: (len %zu)\n", __func__, dlen);
302
  /* sshbuf_dump_data(data, datalen, stderr); */
303
  fprintf(stderr, "%s: sig_r: %s\n", __func__, (tmp = BN_bn2hex(sig_r)));
304
  free(tmp);
305
  fprintf(stderr, "%s: sig_s: %s\n", __func__, (tmp = BN_bn2hex(sig_s)));
306
  free(tmp);
307
  fprintf(stderr, "%s: sig_flags = 0x%02x, sig_counter = %u\n",
308
      __func__, sig_flags, sig_counter);
309
  if (is_webauthn) {
310
    fprintf(stderr, "%s: webauthn origin: %s\n", __func__,
311
        webauthn_origin);
312
    fprintf(stderr, "%s: webauthn_wrapper:\n", __func__);
313
    sshbuf_dump(webauthn_wrapper, stderr);
314
  }
315
#endif
316
0
  if ((esig = ECDSA_SIG_new()) == NULL) {
317
0
    ret = SSH_ERR_ALLOC_FAIL;
318
0
    goto out;
319
0
  }
320
0
  if (!ECDSA_SIG_set0(esig, sig_r, sig_s)) {
321
0
    ret = SSH_ERR_LIBCRYPTO_ERROR;
322
0
    goto out;
323
0
  }
324
0
  sig_r = sig_s = NULL; /* transferred */
325
326
  /* Reconstruct data that was supposedly signed */
327
0
  if ((original_signed = sshbuf_new()) == NULL) {
328
0
    ret = SSH_ERR_ALLOC_FAIL;
329
0
    goto out;
330
0
  }
331
0
  if (is_webauthn) {
332
0
    if ((ret = webauthn_check_prepare_hash(data, dlen,
333
0
        webauthn_origin, webauthn_wrapper, sig_flags, webauthn_exts,
334
0
        msghash, sizeof(msghash))) != 0)
335
0
      goto out;
336
0
  } else if ((ret = ssh_digest_memory(SSH_DIGEST_SHA256, data, dlen,
337
0
      msghash, sizeof(msghash))) != 0)
338
0
    goto out;
339
  /* Application value is hashed before signature */
340
0
  if ((ret = ssh_digest_memory(SSH_DIGEST_SHA256, key->sk_application,
341
0
      strlen(key->sk_application), apphash, sizeof(apphash))) != 0)
342
0
    goto out;
343
#ifdef DEBUG_SK
344
  fprintf(stderr, "%s: hashed application:\n", __func__);
345
  sshbuf_dump_data(apphash, sizeof(apphash), stderr);
346
  fprintf(stderr, "%s: hashed message:\n", __func__);
347
  sshbuf_dump_data(msghash, sizeof(msghash), stderr);
348
#endif
349
0
  if ((ret = sshbuf_put(original_signed,
350
0
      apphash, sizeof(apphash))) != 0 ||
351
0
      (ret = sshbuf_put_u8(original_signed, sig_flags)) != 0 ||
352
0
      (ret = sshbuf_put_u32(original_signed, sig_counter)) != 0 ||
353
0
      (ret = sshbuf_putb(original_signed, webauthn_exts)) != 0 ||
354
0
      (ret = sshbuf_put(original_signed, msghash, sizeof(msghash))) != 0)
355
0
    goto out;
356
0
  details->sk_counter = sig_counter;
357
0
  details->sk_flags = sig_flags;
358
#ifdef DEBUG_SK
359
  fprintf(stderr, "%s: signed buf:\n", __func__);
360
  sshbuf_dump(original_signed, stderr);
361
#endif
362
363
0
  if ((md_ctx = EVP_MD_CTX_new()) == NULL) {
364
0
    ret = SSH_ERR_ALLOC_FAIL;
365
0
    goto out;
366
0
  }
367
0
  if ((len = i2d_ECDSA_SIG(esig, NULL)) <= 0) {
368
0
    len = 0;
369
0
    ret = SSH_ERR_LIBCRYPTO_ERROR;
370
0
    goto out;
371
0
  }
372
0
  if ((sigb = calloc(1, len)) == NULL) {
373
0
    ret = SSH_ERR_ALLOC_FAIL;
374
0
    goto out;
375
0
  }
376
0
  cp = sigb; /* ASN1_item_i2d increments the pointer past the object */
377
0
  if (i2d_ECDSA_SIG(esig, &cp) != len) {
378
0
    ret = SSH_ERR_LIBCRYPTO_ERROR;
379
0
    goto out;
380
0
  }
381
#ifdef DEBUG_SK
382
  fprintf(stderr, "%s: signed hash:\n", __func__);
383
  sshbuf_dump_data(sigb, len, stderr);
384
#endif
385
  /* Verify it */
386
0
  if (EVP_DigestVerifyInit(md_ctx, NULL, EVP_sha256(), NULL,
387
0
      key->pkey) != 1) {
388
0
    ret = SSH_ERR_LIBCRYPTO_ERROR;
389
0
    goto out;
390
0
  }
391
0
  switch (EVP_DigestVerify(md_ctx, sigb, len,
392
0
      sshbuf_ptr(original_signed), sshbuf_len(original_signed))) {
393
0
  case 1:
394
0
    ret = 0;
395
0
    break;
396
0
  case 0:
397
0
    ret = SSH_ERR_SIGNATURE_INVALID;
398
0
    goto out;
399
0
  default:
400
0
    ret = SSH_ERR_LIBCRYPTO_ERROR;
401
0
    goto out;
402
0
  }
403
  /* success */
404
0
  if (detailsp != NULL) {
405
0
    *detailsp = details;
406
0
    details = NULL;
407
0
  }
408
0
 out:
409
0
  explicit_bzero(&sig_flags, sizeof(sig_flags));
410
0
  explicit_bzero(&sig_counter, sizeof(sig_counter));
411
0
  explicit_bzero(msghash, sizeof(msghash));
412
0
  explicit_bzero(apphash, sizeof(apphash));
413
0
  sshkey_sig_details_free(details);
414
0
  sshbuf_free(webauthn_wrapper);
415
0
  sshbuf_free(webauthn_exts);
416
0
  free(webauthn_origin);
417
0
  sshbuf_free(original_signed);
418
0
  sshbuf_free(sigbuf);
419
0
  sshbuf_free(b);
420
0
  ECDSA_SIG_free(esig);
421
0
  BN_clear_free(sig_r);
422
0
  BN_clear_free(sig_s);
423
0
  free(ktype);
424
0
  freezero(sigb, len);
425
0
  EVP_MD_CTX_free(md_ctx);
426
0
  return ret;
427
0
}
428
429
static const struct sshkey_impl_funcs sshkey_ecdsa_sk_funcs = {
430
  /* .size = */   NULL,
431
  /* .alloc = */    NULL,
432
  /* .cleanup = */  ssh_ecdsa_sk_cleanup,
433
  /* .equal = */    ssh_ecdsa_sk_equal,
434
  /* .ssh_serialize_public = */ ssh_ecdsa_sk_serialize_public,
435
  /* .ssh_deserialize_public = */ ssh_ecdsa_sk_deserialize_public,
436
  /* .ssh_serialize_private = */ ssh_ecdsa_sk_serialize_private,
437
  /* .ssh_deserialize_private = */ ssh_ecdsa_sk_deserialize_private,
438
  /* .generate = */ NULL,
439
  /* .copy_public = */  ssh_ecdsa_sk_copy_public,
440
  /* .sign = */   NULL,
441
  /* .verify = */   ssh_ecdsa_sk_verify,
442
};
443
444
const struct sshkey_impl sshkey_ecdsa_sk_impl = {
445
  /* .name = */   "sk-ecdsa-sha2-nistp256@openssh.com",
446
  /* .shortname = */  "ECDSA-SK",
447
  /* .sigalg = */   NULL,
448
  /* .type = */   KEY_ECDSA_SK,
449
  /* .nid = */    NID_X9_62_prime256v1,
450
  /* .cert = */   0,
451
  /* .sigonly = */  0,
452
  /* .keybits = */  256,
453
  /* .funcs = */    &sshkey_ecdsa_sk_funcs,
454
};
455
456
const struct sshkey_impl sshkey_ecdsa_sk_cert_impl = {
457
  /* .name = */   "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com",
458
  /* .shortname = */  "ECDSA-SK-CERT",
459
  /* .sigalg = */   NULL,
460
  /* .type = */   KEY_ECDSA_SK_CERT,
461
  /* .nid = */    NID_X9_62_prime256v1,
462
  /* .cert = */   1,
463
  /* .sigonly = */  0,
464
  /* .keybits = */  256,
465
  /* .funcs = */    &sshkey_ecdsa_sk_funcs,
466
};
467
468
const struct sshkey_impl sshkey_ecdsa_sk_webauthn_impl = {
469
  /* .name = */   "webauthn-sk-ecdsa-sha2-nistp256@openssh.com",
470
  /* .shortname = */  "ECDSA-SK",
471
  /* .sigalg = */   NULL,
472
  /* .type = */   KEY_ECDSA_SK,
473
  /* .nid = */    NID_X9_62_prime256v1,
474
  /* .cert = */   0,
475
  /* .sigonly = */  1,
476
  /* .keybits = */  256,
477
  /* .funcs = */    &sshkey_ecdsa_sk_funcs,
478
};
479
480
const struct sshkey_impl sshkey_ecdsa_sk_webauthn_cert_impl = {
481
  /* .name = */   "webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.com",
482
  /* .shortname = */  "ECDSA-SK-CERT",
483
  /* .sigalg = */   NULL,
484
  /* .type = */   KEY_ECDSA_SK_CERT,
485
  /* .nid = */    NID_X9_62_prime256v1,
486
  /* .cert = */   1,
487
  /* .sigonly = */  1,
488
  /* .keybits = */  256,
489
  /* .funcs = */    &sshkey_ecdsa_sk_funcs,
490
};
491
#endif /* WITH_OPENSSL */