1 /* $NetBSD: ssh-ecdsa-sk.c,v 1.5 2024/09/24 21:32:19 christos Exp $ */ 2 /* $OpenBSD: ssh-ecdsa-sk.c,v 1.19 2024/08/15 00:51:51 djm 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.5 2024/09/24 21:32:19 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 premable:\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@openssh.com") == 0) 263 is_webauthn = 1; 264 else if (strcmp(ktype, "sk-ecdsa-sha2-nistp256@openssh.com") != 0) { 265 ret = SSH_ERR_INVALID_FORMAT; 266 goto out; 267 } 268 if (sshbuf_froms(b, &sigbuf) != 0 || 269 sshbuf_get_u8(b, &sig_flags) != 0 || 270 sshbuf_get_u32(b, &sig_counter) != 0) { 271 ret = SSH_ERR_INVALID_FORMAT; 272 goto out; 273 } 274 if (is_webauthn) { 275 if (sshbuf_get_cstring(b, &webauthn_origin, NULL) != 0 || 276 sshbuf_froms(b, &webauthn_wrapper) != 0 || 277 sshbuf_froms(b, &webauthn_exts) != 0) { 278 ret = SSH_ERR_INVALID_FORMAT; 279 goto out; 280 } 281 } 282 if (sshbuf_len(b) != 0) { 283 ret = SSH_ERR_UNEXPECTED_TRAILING_DATA; 284 goto out; 285 } 286 287 /* parse signature */ 288 if (sshbuf_get_bignum2(sigbuf, &sig_r) != 0 || 289 sshbuf_get_bignum2(sigbuf, &sig_s) != 0) { 290 ret = SSH_ERR_INVALID_FORMAT; 291 goto out; 292 } 293 if (sshbuf_len(sigbuf) != 0) { 294 ret = SSH_ERR_UNEXPECTED_TRAILING_DATA; 295 goto out; 296 } 297 298 #ifdef DEBUG_SK 299 fprintf(stderr, "%s: data: (len %zu)\n", __func__, datalen); 300 /* sshbuf_dump_data(data, datalen, stderr); */ 301 fprintf(stderr, "%s: sig_r: %s\n", __func__, (tmp = BN_bn2hex(sig_r))); 302 free(tmp); 303 fprintf(stderr, "%s: sig_s: %s\n", __func__, (tmp = BN_bn2hex(sig_s))); 304 free(tmp); 305 fprintf(stderr, "%s: sig_flags = 0x%02x, sig_counter = %u\n", 306 __func__, sig_flags, sig_counter); 307 if (is_webauthn) { 308 fprintf(stderr, "%s: webauthn origin: %s\n", __func__, 309 webauthn_origin); 310 fprintf(stderr, "%s: webauthn_wrapper:\n", __func__); 311 sshbuf_dump(webauthn_wrapper, stderr); 312 } 313 #endif 314 if ((esig = ECDSA_SIG_new()) == NULL) { 315 ret = SSH_ERR_ALLOC_FAIL; 316 goto out; 317 } 318 if (!ECDSA_SIG_set0(esig, sig_r, sig_s)) { 319 ret = SSH_ERR_LIBCRYPTO_ERROR; 320 goto out; 321 } 322 sig_r = sig_s = NULL; /* transferred */ 323 324 /* Reconstruct data that was supposedly signed */ 325 if ((original_signed = sshbuf_new()) == NULL) { 326 ret = SSH_ERR_ALLOC_FAIL; 327 goto out; 328 } 329 if (is_webauthn) { 330 if ((ret = webauthn_check_prepare_hash(data, dlen, 331 webauthn_origin, webauthn_wrapper, sig_flags, webauthn_exts, 332 msghash, sizeof(msghash))) != 0) 333 goto out; 334 } else if ((ret = ssh_digest_memory(SSH_DIGEST_SHA256, data, dlen, 335 msghash, sizeof(msghash))) != 0) 336 goto out; 337 /* Application value is hashed before signature */ 338 if ((ret = ssh_digest_memory(SSH_DIGEST_SHA256, key->sk_application, 339 strlen(key->sk_application), apphash, sizeof(apphash))) != 0) 340 goto out; 341 #ifdef DEBUG_SK 342 fprintf(stderr, "%s: hashed application:\n", __func__); 343 sshbuf_dump_data(apphash, sizeof(apphash), stderr); 344 fprintf(stderr, "%s: hashed message:\n", __func__); 345 sshbuf_dump_data(msghash, sizeof(msghash), stderr); 346 #endif 347 if ((ret = sshbuf_put(original_signed, 348 apphash, sizeof(apphash))) != 0 || 349 (ret = sshbuf_put_u8(original_signed, sig_flags)) != 0 || 350 (ret = sshbuf_put_u32(original_signed, sig_counter)) != 0 || 351 (ret = sshbuf_putb(original_signed, webauthn_exts)) != 0 || 352 (ret = sshbuf_put(original_signed, msghash, sizeof(msghash))) != 0) 353 goto out; 354 details->sk_counter = sig_counter; 355 details->sk_flags = sig_flags; 356 #ifdef DEBUG_SK 357 fprintf(stderr, "%s: signed buf:\n", __func__); 358 sshbuf_dump(original_signed, stderr); 359 #endif 360 361 if ((md_ctx = EVP_MD_CTX_new()) == NULL) { 362 ret = SSH_ERR_ALLOC_FAIL; 363 goto out; 364 } 365 if ((len = i2d_ECDSA_SIG(esig, NULL)) <= 0) { 366 len = 0; 367 ret = SSH_ERR_LIBCRYPTO_ERROR; 368 goto out; 369 } 370 if ((sigb = calloc(1, len)) == NULL) { 371 ret = SSH_ERR_ALLOC_FAIL; 372 goto out; 373 } 374 cp = sigb; /* ASN1_item_i2d increments the pointer past the object */ 375 if (i2d_ECDSA_SIG(esig, &cp) != len) { 376 ret = SSH_ERR_LIBCRYPTO_ERROR; 377 goto out; 378 } 379 #ifdef DEBUG_SK 380 fprintf(stderr, "%s: signed hash:\n", __func__); 381 sshbuf_dump_data(sigb, len, stderr); 382 #endif 383 /* Verify it */ 384 if (EVP_DigestVerifyInit(md_ctx, NULL, EVP_sha256(), NULL, 385 key->pkey) != 1) { 386 ret = SSH_ERR_LIBCRYPTO_ERROR; 387 goto out; 388 } 389 switch (EVP_DigestVerify(md_ctx, sigb, len, 390 sshbuf_ptr(original_signed), sshbuf_len(original_signed))) { 391 case 1: 392 ret = 0; 393 break; 394 case 0: 395 ret = SSH_ERR_SIGNATURE_INVALID; 396 goto out; 397 default: 398 ret = SSH_ERR_LIBCRYPTO_ERROR; 399 goto out; 400 } 401 /* success */ 402 if (detailsp != NULL) { 403 *detailsp = details; 404 details = NULL; 405 } 406 out: 407 explicit_bzero(&sig_flags, sizeof(sig_flags)); 408 explicit_bzero(&sig_counter, sizeof(sig_counter)); 409 explicit_bzero(msghash, sizeof(msghash)); 410 explicit_bzero(apphash, sizeof(apphash)); 411 sshkey_sig_details_free(details); 412 sshbuf_free(webauthn_wrapper); 413 sshbuf_free(webauthn_exts); 414 free(webauthn_origin); 415 sshbuf_free(original_signed); 416 sshbuf_free(sigbuf); 417 sshbuf_free(b); 418 ECDSA_SIG_free(esig); 419 BN_clear_free(sig_r); 420 BN_clear_free(sig_s); 421 free(ktype); 422 freezero(sigb, len); 423 EVP_MD_CTX_free(md_ctx); 424 return ret; 425 } 426 427 static const struct sshkey_impl_funcs sshkey_ecdsa_sk_funcs = { 428 /* .size = */ NULL, 429 /* .alloc = */ NULL, 430 /* .cleanup = */ ssh_ecdsa_sk_cleanup, 431 /* .equal = */ ssh_ecdsa_sk_equal, 432 /* .ssh_serialize_public = */ ssh_ecdsa_sk_serialize_public, 433 /* .ssh_deserialize_public = */ ssh_ecdsa_sk_deserialize_public, 434 /* .ssh_serialize_private = */ ssh_ecdsa_sk_serialize_private, 435 /* .ssh_deserialize_private = */ ssh_ecdsa_sk_deserialize_private, 436 /* .generate = */ NULL, 437 /* .copy_public = */ ssh_ecdsa_sk_copy_public, 438 /* .sign = */ NULL, 439 /* .verify = */ ssh_ecdsa_sk_verify, 440 }; 441 442 const struct sshkey_impl sshkey_ecdsa_sk_impl = { 443 /* .name = */ "sk-ecdsa-sha2-nistp256@openssh.com", 444 /* .shortname = */ "ECDSA-SK", 445 /* .sigalg = */ NULL, 446 /* .type = */ KEY_ECDSA_SK, 447 /* .nid = */ NID_X9_62_prime256v1, 448 /* .cert = */ 0, 449 /* .sigonly = */ 0, 450 /* .keybits = */ 256, 451 /* .funcs = */ &sshkey_ecdsa_sk_funcs, 452 }; 453 454 const struct sshkey_impl sshkey_ecdsa_sk_cert_impl = { 455 /* .name = */ "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com", 456 /* .shortname = */ "ECDSA-SK-CERT", 457 /* .sigalg = */ NULL, 458 /* .type = */ KEY_ECDSA_SK_CERT, 459 /* .nid = */ NID_X9_62_prime256v1, 460 /* .cert = */ 1, 461 /* .sigonly = */ 0, 462 /* .keybits = */ 256, 463 /* .funcs = */ &sshkey_ecdsa_sk_funcs, 464 }; 465 466 const struct sshkey_impl sshkey_ecdsa_sk_webauthn_impl = { 467 /* .name = */ "webauthn-sk-ecdsa-sha2-nistp256@openssh.com", 468 /* .shortname = */ "ECDSA-SK", 469 /* .sigalg = */ NULL, 470 /* .type = */ KEY_ECDSA_SK, 471 /* .nid = */ NID_X9_62_prime256v1, 472 /* .cert = */ 0, 473 /* .sigonly = */ 1, 474 /* .keybits = */ 256, 475 /* .funcs = */ &sshkey_ecdsa_sk_funcs, 476 }; 477