1 /* $OpenBSD: ssh-verify-attestation.c,v 1.2 2024/12/06 10:37:42 djm Exp $ */ 2 /* 3 * Copyright (c) 2022-2024 Damien Miller 4 * 5 * Permission to use, copy, modify, and distribute this software for any 6 * purpose with or without fee is hereby granted, provided that the above 7 * copyright notice and this permission notice appear in all copies. 8 * 9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 */ 17 18 /* 19 * This is a small program to verify FIDO attestation objects that 20 * ssh-keygen(1) can record when enrolling a FIDO key. It requires that 21 * the attestation object and challenge used when creating the key be 22 * recorded. 23 * 24 * Example usage: 25 * 26 * $ # Generate a random challenge. 27 * $ dd if=/dev/urandom of=key_ecdsa_sk.challenge bs=32 count=1 28 * $ # Generate a key, record the attestation blob. 29 * $ ssh-keygen -f key_ecdsa_sk -t ecdsa-sk \ 30 * -Ochallenge=key_ecdsa_sk.challenge \ 31 * -Owrite-attestation=key_ecdsa_sk.attest -N '' 32 * $ # Validate the challenge (-A = print attestation CA cert) 33 * $ ./obj/ssh-verify-attestation -A key_ecdsa_sk key_ecdsa_sk.challenge \ 34 * key_ecdsa_sk.attest 35 * 36 * Limitations/TODO: 37 * 38 * 1) It doesn't automatically detect the attestation statement format. It 39 * assumes the "packed" format used by FIDO2 keys. If that doesn't work, 40 * then try using the -U option to select the "fido-u2f" format. 41 * 2) It makes assumptions about RK, UV, etc status of the key/cred. 42 * 3) Probably bugs. 43 * 44 * Thanks to Markus Friedl and Pedro Martelletto for help getting this 45 * working. 46 */ 47 48 #include "includes.h" 49 50 #include <stdint.h> 51 #include <inttypes.h> 52 #include <stdlib.h> 53 #include <stdio.h> 54 #include <unistd.h> 55 #include <stdarg.h> 56 57 #include "xmalloc.h" 58 #include "log.h" 59 #include "sshbuf.h" 60 #include "sshkey.h" 61 #include "authfile.h" 62 #include "ssherr.h" 63 #include "misc.h" 64 #include "digest.h" 65 #include "crypto_api.h" 66 67 #include <fido.h> 68 #include <openssl/x509.h> 69 #include <openssl/x509v3.h> 70 #include <openssl/bio.h> 71 #include <openssl/err.h> 72 #include <openssl/pem.h> 73 74 extern char *__progname; 75 76 #define ATTEST_MAGIC "ssh-sk-attest-v01" 77 78 static int 79 prepare_fido_cred(fido_cred_t *cred, int credtype, const char *attfmt, 80 const char *rp_id, struct sshbuf *b, const struct sshbuf *challenge, 81 struct sshbuf **attestation_certp) 82 { 83 struct sshbuf *attestation_cert = NULL, *sig = NULL, *authdata = NULL; 84 char *magic = NULL; 85 int r = SSH_ERR_INTERNAL_ERROR; 86 87 *attestation_certp = NULL; 88 89 /* Make sure it's the format we're expecting */ 90 if ((r = sshbuf_get_cstring(b, &magic, NULL)) != 0) { 91 error_fr(r, "parse header"); 92 goto out; 93 } 94 if (strcmp(magic, ATTEST_MAGIC) != 0) { 95 error_f("unsupported format"); 96 r = SSH_ERR_INVALID_FORMAT; 97 goto out; 98 } 99 /* Parse the remaining fields */ 100 if ((r = sshbuf_froms(b, &attestation_cert)) != 0 || 101 (r = sshbuf_froms(b, &sig)) != 0 || 102 (r = sshbuf_froms(b, &authdata)) != 0 || 103 (r = sshbuf_get_u32(b, NULL)) != 0 || /* reserved flags */ 104 (r = sshbuf_get_string_direct(b, NULL, NULL)) != 0) { /* reserved */ 105 error_fr(r, "parse body"); 106 goto out; 107 } 108 debug3_f("attestation cert len=%zu, sig len=%zu, " 109 "authdata len=%zu challenge len=%zu", sshbuf_len(attestation_cert), 110 sshbuf_len(sig), sshbuf_len(authdata), sshbuf_len(challenge)); 111 112 fido_cred_set_type(cred, credtype); 113 fido_cred_set_fmt(cred, attfmt); 114 fido_cred_set_clientdata(cred, sshbuf_ptr(challenge), 115 sshbuf_len(challenge)); 116 fido_cred_set_rp(cred, rp_id, NULL); 117 fido_cred_set_authdata(cred, sshbuf_ptr(authdata), 118 sshbuf_len(authdata)); 119 /* XXX set_extensions, set_rk, set_uv */ 120 fido_cred_set_x509(cred, sshbuf_ptr(attestation_cert), 121 sshbuf_len(attestation_cert)); 122 fido_cred_set_sig(cred, sshbuf_ptr(sig), sshbuf_len(sig)); 123 124 /* success */ 125 *attestation_certp = attestation_cert; 126 attestation_cert = NULL; 127 r = 0; 128 out: 129 free(magic); 130 sshbuf_free(attestation_cert); 131 sshbuf_free(sig); 132 sshbuf_free(authdata); 133 return r; 134 } 135 136 static uint8_t * 137 get_pubkey_from_cred_ecdsa(const fido_cred_t *cred, size_t *pubkey_len) 138 { 139 const uint8_t *ptr; 140 uint8_t *pubkey = NULL, *ret = NULL; 141 BIGNUM *x = NULL, *y = NULL; 142 EC_POINT *q = NULL; 143 EC_GROUP *g = NULL; 144 145 if ((x = BN_new()) == NULL || 146 (y = BN_new()) == NULL || 147 (g = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)) == NULL || 148 (q = EC_POINT_new(g)) == NULL) { 149 error_f("libcrypto setup failed"); 150 goto out; 151 } 152 if ((ptr = fido_cred_pubkey_ptr(cred)) == NULL) { 153 error_f("fido_cred_pubkey_ptr failed"); 154 goto out; 155 } 156 if (fido_cred_pubkey_len(cred) != 64) { 157 error_f("bad fido_cred_pubkey_len %zu", 158 fido_cred_pubkey_len(cred)); 159 goto out; 160 } 161 162 if (BN_bin2bn(ptr, 32, x) == NULL || 163 BN_bin2bn(ptr + 32, 32, y) == NULL) { 164 error_f("BN_bin2bn failed"); 165 goto out; 166 } 167 if (EC_POINT_set_affine_coordinates_GFp(g, q, x, y, NULL) != 1) { 168 error_f("EC_POINT_set_affine_coordinates_GFp failed"); 169 goto out; 170 } 171 *pubkey_len = EC_POINT_point2oct(g, q, 172 POINT_CONVERSION_UNCOMPRESSED, NULL, 0, NULL); 173 if (*pubkey_len == 0 || *pubkey_len > 2048) { 174 error_f("bad pubkey length %zu", *pubkey_len); 175 goto out; 176 } 177 if ((pubkey = malloc(*pubkey_len)) == NULL) { 178 error_f("malloc pubkey failed"); 179 goto out; 180 } 181 if (EC_POINT_point2oct(g, q, POINT_CONVERSION_UNCOMPRESSED, 182 pubkey, *pubkey_len, NULL) == 0) { 183 error_f("EC_POINT_point2oct failed"); 184 goto out; 185 } 186 /* success */ 187 ret = pubkey; 188 pubkey = NULL; 189 out: 190 free(pubkey); 191 EC_POINT_free(q); 192 EC_GROUP_free(g); 193 BN_clear_free(x); 194 BN_clear_free(y); 195 return ret; 196 } 197 198 /* copied from sshsk_ecdsa_assemble() */ 199 static int 200 cred_matches_key_ecdsa(const fido_cred_t *cred, const struct sshkey *k) 201 { 202 struct sshkey *key = NULL; 203 struct sshbuf *b = NULL; 204 EC_KEY *ec = NULL; 205 uint8_t *pubkey = NULL; 206 size_t pubkey_len; 207 int r; 208 209 if ((key = sshkey_new(KEY_ECDSA_SK)) == NULL) { 210 error_f("sshkey_new failed"); 211 r = SSH_ERR_ALLOC_FAIL; 212 goto out; 213 } 214 key->ecdsa_nid = NID_X9_62_prime256v1; 215 if ((key->pkey = EVP_PKEY_new()) == NULL || 216 (ec = EC_KEY_new_by_curve_name(key->ecdsa_nid)) == NULL || 217 (b = sshbuf_new()) == NULL) { 218 error_f("allocation failed"); 219 r = SSH_ERR_ALLOC_FAIL; 220 goto out; 221 } 222 if ((pubkey = get_pubkey_from_cred_ecdsa(cred, &pubkey_len)) == NULL) { 223 error_f("get_pubkey_from_cred_ecdsa failed"); 224 r = SSH_ERR_INVALID_FORMAT; 225 goto out; 226 } 227 if ((r = sshbuf_put_string(b, pubkey, pubkey_len)) != 0) { 228 error_fr(r, "sshbuf_put_string"); 229 goto out; 230 } 231 if ((r = sshbuf_get_eckey(b, ec)) != 0) { 232 error_fr(r, "parse"); 233 r = SSH_ERR_INVALID_FORMAT; 234 goto out; 235 } 236 if (sshkey_ec_validate_public(EC_KEY_get0_group(ec), 237 EC_KEY_get0_public_key(ec)) != 0) { 238 error("Authenticator returned invalid ECDSA key"); 239 r = SSH_ERR_KEY_INVALID_EC_VALUE; 240 goto out; 241 } 242 if (EVP_PKEY_set1_EC_KEY(key->pkey, ec) != 1) { 243 /* XXX assume it is a allocation error */ 244 error_f("allocation failed"); 245 r = SSH_ERR_ALLOC_FAIL; 246 goto out; 247 } 248 key->sk_application = xstrdup(k->sk_application); /* XXX */ 249 if (!sshkey_equal_public(key, k)) { 250 error("sshkey_equal_public failed"); 251 r = SSH_ERR_INVALID_ARGUMENT; 252 goto out; 253 } 254 r = 0; /* success */ 255 out: 256 EC_KEY_free(ec); 257 free(pubkey); 258 sshkey_free(key); 259 sshbuf_free(b); 260 return r; 261 } 262 263 264 /* copied from sshsk_ed25519_assemble() */ 265 static int 266 cred_matches_key_ed25519(const fido_cred_t *cred, const struct sshkey *k) 267 { 268 struct sshkey *key = NULL; 269 const uint8_t *ptr; 270 int r = -1; 271 272 if ((ptr = fido_cred_pubkey_ptr(cred)) == NULL) { 273 error_f("fido_cred_pubkey_ptr failed"); 274 goto out; 275 } 276 if (fido_cred_pubkey_len(cred) != ED25519_PK_SZ) { 277 error_f("bad fido_cred_pubkey_len %zu", 278 fido_cred_pubkey_len(cred)); 279 goto out; 280 } 281 282 if ((key = sshkey_new(KEY_ED25519_SK)) == NULL) { 283 error_f("sshkey_new failed"); 284 r = SSH_ERR_ALLOC_FAIL; 285 goto out; 286 } 287 if ((key->ed25519_pk = malloc(ED25519_PK_SZ)) == NULL) { 288 error_f("malloc failed"); 289 r = SSH_ERR_ALLOC_FAIL; 290 goto out; 291 } 292 memcpy(key->ed25519_pk, ptr, ED25519_PK_SZ); 293 key->sk_application = xstrdup(k->sk_application); /* XXX */ 294 if (!sshkey_equal_public(key, k)) { 295 error("sshkey_equal_public failed"); 296 r = SSH_ERR_INVALID_ARGUMENT; 297 goto out; 298 } 299 r = 0; /* success */ 300 out: 301 sshkey_free(key); 302 return r; 303 } 304 305 static int 306 cred_matches_key(const fido_cred_t *cred, const struct sshkey *k) 307 { 308 switch (sshkey_type_plain(k->type)) { 309 case KEY_ECDSA_SK: 310 switch (k->ecdsa_nid) { 311 case NID_X9_62_prime256v1: 312 return cred_matches_key_ecdsa(cred, k); 313 break; 314 default: 315 fatal("Unsupported ECDSA key size"); 316 } 317 break; 318 case KEY_ED25519_SK: 319 return cred_matches_key_ed25519(cred, k); 320 default: 321 error_f("key type %s not supported", sshkey_type(k)); 322 return -1; 323 } 324 } 325 326 int 327 main(int argc, char **argv) 328 { 329 LogLevel log_level = SYSLOG_LEVEL_INFO; 330 int r, ch, credtype = -1; 331 struct sshkey *k = NULL; 332 struct sshbuf *attestation = NULL, *challenge = NULL; 333 struct sshbuf *attestation_cert = NULL; 334 char *fp; 335 const char *attfmt = "packed", *style = NULL; 336 fido_cred_t *cred = NULL; 337 int write_attestation_cert = 0; 338 extern int optind; 339 /* extern char *optarg; */ 340 341 ERR_load_crypto_strings(); 342 343 sanitise_stdfd(); 344 log_init(__progname, log_level, SYSLOG_FACILITY_AUTH, 1); 345 346 while ((ch = getopt(argc, argv, "UAv")) != -1) { 347 switch (ch) { 348 case 'U': 349 attfmt = "fido-u2f"; 350 break; 351 case 'A': 352 write_attestation_cert = 1; 353 break; 354 case 'v': 355 if (log_level == SYSLOG_LEVEL_ERROR) 356 log_level = SYSLOG_LEVEL_DEBUG1; 357 else if (log_level < SYSLOG_LEVEL_DEBUG3) 358 log_level++; 359 break; 360 default: 361 goto usage; 362 } 363 } 364 log_init(__progname, log_level, SYSLOG_FACILITY_AUTH, 1); 365 argv += optind; 366 argc -= optind; 367 368 if (argc < 3) { 369 usage: 370 fprintf(stderr, "usage: %s [-vAU] " 371 "pubkey challenge attestation-blob\n", __progname); 372 exit(1); 373 } 374 if ((r = sshkey_load_public(argv[0], &k, NULL)) != 0) 375 fatal_r(r, "load key %s", argv[0]); 376 if ((fp = sshkey_fingerprint(k, SSH_FP_HASH_DEFAULT, 377 SSH_FP_DEFAULT)) == NULL) 378 fatal("sshkey_fingerprint failed"); 379 debug2("key %s: %s %s", argv[2], sshkey_type(k), fp); 380 free(fp); 381 if ((r = sshbuf_load_file(argv[1], &challenge)) != 0) 382 fatal_r(r, "load challenge %s", argv[1]); 383 if ((r = sshbuf_load_file(argv[2], &attestation)) != 0) 384 fatal_r(r, "load attestation %s", argv[2]); 385 if ((cred = fido_cred_new()) == NULL) 386 fatal("fido_cred_new failed"); 387 388 switch (sshkey_type_plain(k->type)) { 389 case KEY_ECDSA_SK: 390 switch (k->ecdsa_nid) { 391 case NID_X9_62_prime256v1: 392 credtype = COSE_ES256; 393 break; 394 default: 395 fatal("Unsupported ECDSA key size"); 396 } 397 break; 398 case KEY_ED25519_SK: 399 credtype = COSE_EDDSA; 400 break; 401 default: 402 fatal("unsupported key type %s", sshkey_type(k)); 403 } 404 405 if ((r = prepare_fido_cred(cred, credtype, attfmt, k->sk_application, 406 attestation, challenge, &attestation_cert)) != 0) 407 fatal_r(r, "prepare_fido_cred %s", argv[2]); 408 if (fido_cred_x5c_ptr(cred) != NULL) { 409 debug("basic attestation"); 410 if ((r = fido_cred_verify(cred)) != FIDO_OK) 411 fatal("basic attestation failed"); 412 style = "basic"; 413 } else { 414 debug("self attestation"); 415 if ((r = fido_cred_verify_self(cred)) != FIDO_OK) 416 fatal("self attestation failed"); 417 style = "self"; 418 } 419 if (cred_matches_key(cred, k) != 0) 420 fatal("cred authdata does not match key"); 421 422 fido_cred_free(&cred); 423 424 if (write_attestation_cert) { 425 PEM_write(stdout, "CERTIFICATE", NULL, 426 sshbuf_ptr(attestation_cert), sshbuf_len(attestation_cert)); 427 } 428 sshbuf_free(attestation_cert); 429 430 logit("%s: verified %s attestation", argv[2], style); 431 432 return (0); 433 } 434