1 /* $NetBSD: ssh-ecdsa-sk.c,v 1.6 2026/04/08 18:58:41 christos Exp $ */ 2 /* $OpenBSD: ssh-ecdsa-sk.c,v 1.21 2026/02/06 22:59:18 dtucker Exp $ */ 3 4 /* 5 * Copyright (c) 2000 Markus Friedl. All rights reserved. 6 * Copyright (c) 2010 Damien Miller. All rights reserved. 7 * Copyright (c) 2019 Google Inc. All rights reserved. 8 * 9 * Redistribution and use in source and binary forms, with or without 10 * modification, are permitted provided that the following conditions 11 * are met: 12 * 1. Redistributions of source code must retain the above copyright 13 * notice, this list of conditions and the following disclaimer. 14 * 2. Redistributions in binary form must reproduce the above copyright 15 * notice, this list of conditions and the following disclaimer in the 16 * documentation and/or other materials provided with the distribution. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 19 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 20 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 21 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 22 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 23 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 */ 29 #include "includes.h" 30 __RCSID("$NetBSD: ssh-ecdsa-sk.c,v 1.6 2026/04/08 18:58:41 christos Exp $"); 31 32 /* #define DEBUG_SK 1 */ 33 34 #include <sys/types.h> 35 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 { 56 sshkey_sk_cleanup(k); 57 sshkey_ecdsa_funcs.cleanup(k); 58 } 59 60 static int 61 ssh_ecdsa_sk_equal(const struct sshkey *a, const struct sshkey *b) 62 { 63 if (!sshkey_sk_fields_equal(a, b)) 64 return 0; 65 if (!sshkey_ecdsa_funcs.equal(a, b)) 66 return 0; 67 return 1; 68 } 69 70 static int 71 ssh_ecdsa_sk_serialize_public(const struct sshkey *key, struct sshbuf *b, 72 enum sshkey_serialize_rep opts) 73 { 74 int r; 75 76 if ((r = sshkey_ecdsa_funcs.serialize_public(key, b, opts)) != 0) 77 return r; 78 if ((r = sshkey_serialize_sk(key, b)) != 0) 79 return r; 80 81 return 0; 82 } 83 84 static int 85 ssh_ecdsa_sk_serialize_private(const struct sshkey *key, struct sshbuf *b, 86 enum sshkey_serialize_rep opts) 87 { 88 int r; 89 90 if (!sshkey_is_cert(key)) { 91 if ((r = sshkey_ecdsa_funcs.serialize_public(key, 92 b, opts)) != 0) 93 return r; 94 } 95 if ((r = sshkey_serialize_private_sk(key, b)) != 0) 96 return r; 97 98 return 0; 99 } 100 101 static int 102 ssh_ecdsa_sk_copy_public(const struct sshkey *from, struct sshkey *to) 103 { 104 int r; 105 106 if ((r = sshkey_ecdsa_funcs.copy_public(from, to)) != 0) 107 return r; 108 if ((r = sshkey_copy_public_sk(from, to)) != 0) 109 return r; 110 return 0; 111 } 112 113 static int 114 ssh_ecdsa_sk_deserialize_public(const char *ktype, struct sshbuf *b, 115 struct sshkey *key) 116 { 117 int r; 118 119 if ((r = sshkey_ecdsa_funcs.deserialize_public(ktype, b, key)) != 0) 120 return r; 121 if ((r = sshkey_deserialize_sk(b, key)) != 0) 122 return r; 123 return 0; 124 } 125 126 static int 127 ssh_ecdsa_sk_deserialize_private(const char *ktype, struct sshbuf *b, 128 struct sshkey *key) 129 { 130 int r; 131 132 if (!sshkey_is_cert(key)) { 133 if ((r = sshkey_ecdsa_funcs.deserialize_public(ktype, 134 b, key)) != 0) 135 return r; 136 } 137 if ((r = sshkey_private_deserialize_sk(b, key)) != 0) 138 return r; 139 140 return 0; 141 } 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 { 161 int r = SSH_ERR_INTERNAL_ERROR; 162 struct sshbuf *chall = NULL, *m = NULL; 163 164 if ((m = sshbuf_new()) == NULL || 165 (chall = sshbuf_from(data, datalen)) == NULL) { 166 r = SSH_ERR_ALLOC_FAIL; 167 goto out; 168 } 169 /* 170 * Ensure origin contains no quote character and that the flags are 171 * consistent with what we received 172 */ 173 if (strchr(origin, '\"') != NULL || 174 (flags & 0x40) != 0 /* AD */ || 175 ((flags & 0x80) == 0 /* ED */) != (sshbuf_len(extensions) == 0)) { 176 r = SSH_ERR_INVALID_FORMAT; 177 goto out; 178 } 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 #define WEBAUTHN_0 "{\"type\":\"webauthn.get\",\"challenge\":\"" 187 #define WEBAUTHN_1 "\",\"origin\":\"" 188 #define WEBAUTHN_2 "\"" 189 if ((r = sshbuf_put(m, WEBAUTHN_0, sizeof(WEBAUTHN_0) - 1)) != 0 || 190 (r = sshbuf_dtourlb64(chall, m, 0)) != 0 || 191 (r = sshbuf_put(m, WEBAUTHN_1, sizeof(WEBAUTHN_1) - 1)) != 0 || 192 (r = sshbuf_put(m, origin, strlen(origin))) != 0 || 193 (r = sshbuf_put(m, WEBAUTHN_2, sizeof(WEBAUTHN_2) - 1)) != 0) 194 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 if ((r = sshbuf_cmp(wrapper, 0, sshbuf_ptr(m), sshbuf_len(m))) != 0) 204 goto out; 205 206 /* Prepare hash of clientData */ 207 if ((r = ssh_digest_buffer(SSH_DIGEST_SHA256, wrapper, 208 msghash, msghashlen)) != 0) 209 goto out; 210 211 /* success */ 212 r = 0; 213 out: 214 sshbuf_free(chall); 215 sshbuf_free(m); 216 return r; 217 } 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 { 225 ECDSA_SIG *esig = NULL; 226 EVP_MD_CTX *md_ctx = NULL; 227 BIGNUM *sig_r = NULL, *sig_s = NULL; 228 u_char sig_flags; 229 u_char msghash[32], apphash[32]; 230 u_int sig_counter; 231 u_char *sigb = NULL, *cp; 232 int is_webauthn = 0, ret = SSH_ERR_INTERNAL_ERROR, len = 0; 233 struct sshbuf *b = NULL, *sigbuf = NULL, *original_signed = NULL; 234 struct sshbuf *webauthn_wrapper = NULL, *webauthn_exts = NULL; 235 char *ktype = NULL, *webauthn_origin = NULL; 236 struct sshkey_sig_details *details = NULL; 237 #ifdef DEBUG_SK 238 char *tmp = NULL; 239 #endif 240 241 if (detailsp != NULL) 242 *detailsp = NULL; 243 if (key == NULL || key->pkey == NULL || 244 sshkey_type_plain(key->type) != KEY_ECDSA_SK || 245 sig == NULL || siglen == 0) 246 return SSH_ERR_INVALID_ARGUMENT; 247 248 if (key->ecdsa_nid != NID_X9_62_prime256v1) 249 return SSH_ERR_INTERNAL_ERROR; 250 251 /* fetch signature */ 252 if ((b = sshbuf_from(sig, siglen)) == NULL) 253 return SSH_ERR_ALLOC_FAIL; 254 if ((details = calloc(1, sizeof(*details))) == NULL) { 255 ret = SSH_ERR_ALLOC_FAIL; 256 goto out; 257 } 258 if (sshbuf_get_cstring(b, &ktype, NULL) != 0) { 259 ret = SSH_ERR_INVALID_FORMAT; 260 goto out; 261 } 262 if (strcmp(ktype, "webauthn-sk-ecdsa-sha2-nistp256 (at) openssh.com") == 0 || 263 strcmp(ktype, "webauthn-sk-ecdsa-sha2-nistp256-cert-v01 (at) openssh.com") 264 == 0) 265 is_webauthn = 1; 266 else if (strcmp(ktype, "sk-ecdsa-sha2-nistp256 (at) openssh.com") != 0) { 267 ret = SSH_ERR_INVALID_FORMAT; 268 goto out; 269 } 270 if (sshbuf_froms(b, &sigbuf) != 0 || 271 sshbuf_get_u8(b, &sig_flags) != 0 || 272 sshbuf_get_u32(b, &sig_counter) != 0) { 273 ret = SSH_ERR_INVALID_FORMAT; 274 goto out; 275 } 276 if (is_webauthn) { 277 if (sshbuf_get_cstring(b, &webauthn_origin, NULL) != 0 || 278 sshbuf_froms(b, &webauthn_wrapper) != 0 || 279 sshbuf_froms(b, &webauthn_exts) != 0) { 280 ret = SSH_ERR_INVALID_FORMAT; 281 goto out; 282 } 283 } 284 if (sshbuf_len(b) != 0) { 285 ret = SSH_ERR_UNEXPECTED_TRAILING_DATA; 286 goto out; 287 } 288 289 /* parse signature */ 290 if (sshbuf_get_bignum2(sigbuf, &sig_r) != 0 || 291 sshbuf_get_bignum2(sigbuf, &sig_s) != 0) { 292 ret = SSH_ERR_INVALID_FORMAT; 293 goto out; 294 } 295 if (sshbuf_len(sigbuf) != 0) { 296 ret = SSH_ERR_UNEXPECTED_TRAILING_DATA; 297 goto out; 298 } 299 300 #ifdef DEBUG_SK 301 fprintf(stderr, "%s: data: (len %zu)\n", __func__, datalen); 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 if ((esig = ECDSA_SIG_new()) == NULL) { 317 ret = SSH_ERR_ALLOC_FAIL; 318 goto out; 319 } 320 if (!ECDSA_SIG_set0(esig, sig_r, sig_s)) { 321 ret = SSH_ERR_LIBCRYPTO_ERROR; 322 goto out; 323 } 324 sig_r = sig_s = NULL; /* transferred */ 325 326 /* Reconstruct data that was supposedly signed */ 327 if ((original_signed = sshbuf_new()) == NULL) { 328 ret = SSH_ERR_ALLOC_FAIL; 329 goto out; 330 } 331 if (is_webauthn) { 332 if ((ret = webauthn_check_prepare_hash(data, dlen, 333 webauthn_origin, webauthn_wrapper, sig_flags, webauthn_exts, 334 msghash, sizeof(msghash))) != 0) 335 goto out; 336 } else if ((ret = ssh_digest_memory(SSH_DIGEST_SHA256, data, dlen, 337 msghash, sizeof(msghash))) != 0) 338 goto out; 339 /* Application value is hashed before signature */ 340 if ((ret = ssh_digest_memory(SSH_DIGEST_SHA256, key->sk_application, 341 strlen(key->sk_application), apphash, sizeof(apphash))) != 0) 342 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 if ((ret = sshbuf_put(original_signed, 350 apphash, sizeof(apphash))) != 0 || 351 (ret = sshbuf_put_u8(original_signed, sig_flags)) != 0 || 352 (ret = sshbuf_put_u32(original_signed, sig_counter)) != 0 || 353 (ret = sshbuf_putb(original_signed, webauthn_exts)) != 0 || 354 (ret = sshbuf_put(original_signed, msghash, sizeof(msghash))) != 0) 355 goto out; 356 details->sk_counter = sig_counter; 357 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 if ((md_ctx = EVP_MD_CTX_new()) == NULL) { 364 ret = SSH_ERR_ALLOC_FAIL; 365 goto out; 366 } 367 if ((len = i2d_ECDSA_SIG(esig, NULL)) <= 0) { 368 len = 0; 369 ret = SSH_ERR_LIBCRYPTO_ERROR; 370 goto out; 371 } 372 if ((sigb = calloc(1, len)) == NULL) { 373 ret = SSH_ERR_ALLOC_FAIL; 374 goto out; 375 } 376 cp = sigb; /* ASN1_item_i2d increments the pointer past the object */ 377 if (i2d_ECDSA_SIG(esig, &cp) != len) { 378 ret = SSH_ERR_LIBCRYPTO_ERROR; 379 goto out; 380 } 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 if (EVP_DigestVerifyInit(md_ctx, NULL, EVP_sha256(), NULL, 387 key->pkey) != 1) { 388 ret = SSH_ERR_LIBCRYPTO_ERROR; 389 goto out; 390 } 391 switch (EVP_DigestVerify(md_ctx, sigb, len, 392 sshbuf_ptr(original_signed), sshbuf_len(original_signed))) { 393 case 1: 394 ret = 0; 395 break; 396 case 0: 397 ret = SSH_ERR_SIGNATURE_INVALID; 398 goto out; 399 default: 400 ret = SSH_ERR_LIBCRYPTO_ERROR; 401 goto out; 402 } 403 /* success */ 404 if (detailsp != NULL) { 405 *detailsp = details; 406 details = NULL; 407 } 408 out: 409 explicit_bzero(&sig_flags, sizeof(sig_flags)); 410 explicit_bzero(&sig_counter, sizeof(sig_counter)); 411 explicit_bzero(msghash, sizeof(msghash)); 412 explicit_bzero(apphash, sizeof(apphash)); 413 sshkey_sig_details_free(details); 414 sshbuf_free(webauthn_wrapper); 415 sshbuf_free(webauthn_exts); 416 free(webauthn_origin); 417 sshbuf_free(original_signed); 418 sshbuf_free(sigbuf); 419 sshbuf_free(b); 420 ECDSA_SIG_free(esig); 421 BN_clear_free(sig_r); 422 BN_clear_free(sig_s); 423 free(ktype); 424 freezero(sigb, len); 425 EVP_MD_CTX_free(md_ctx); 426 return ret; 427 } 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 (at) 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 (at) 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 (at) 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 (at) 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