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 <stdint.h> 49 #include <inttypes.h> 50 #include <stdlib.h> 51 #include <stdio.h> 52 #include <unistd.h> 53 #include <stdarg.h> 54 55 #include "xmalloc.h" 56 #include "log.h" 57 #include "sshbuf.h" 58 #include "sshkey.h" 59 #include "authfile.h" 60 #include "ssherr.h" 61 #include "misc.h" 62 #include "digest.h" 63 #include "crypto_api.h" 64 65 #include <fido.h> 66 #include <openssl/x509.h> 67 #include <openssl/x509v3.h> 68 #include <openssl/bio.h> 69 #include <openssl/err.h> 70 #include <openssl/pem.h> 71 72 extern char *__progname; 73 74 #define ATTEST_MAGIC "ssh-sk-attest-v01" 75 76 static int 77 prepare_fido_cred(fido_cred_t *cred, int credtype, const char *attfmt, 78 const char *rp_id, struct sshbuf *b, const struct sshbuf *challenge, 79 struct sshbuf **attestation_certp) 80 { 81 struct sshbuf *attestation_cert = NULL, *sig = NULL, *authdata = NULL; 82 char *magic = NULL; 83 int r = SSH_ERR_INTERNAL_ERROR; 84 85 *attestation_certp = NULL; 86 87 /* Make sure it's the format we're expecting */ 88 if ((r = sshbuf_get_cstring(b, &magic, NULL)) != 0) { 89 error_fr(r, "parse header"); 90 goto out; 91 } 92 if (strcmp(magic, ATTEST_MAGIC) != 0) { 93 error_f("unsupported format"); 94 r = SSH_ERR_INVALID_FORMAT; 95 goto out; 96 } 97 /* Parse the remaining fields */ 98 if ((r = sshbuf_froms(b, &attestation_cert)) != 0 || 99 (r = sshbuf_froms(b, &sig)) != 0 || 100 (r = sshbuf_froms(b, &authdata)) != 0 || 101 (r = sshbuf_get_u32(b, NULL)) != 0 || /* reserved flags */ 102 (r = sshbuf_get_string_direct(b, NULL, NULL)) != 0) { /* reserved */ 103 error_fr(r, "parse body"); 104 goto out; 105 } 106 debug3_f("attestation cert len=%zu, sig len=%zu, " 107 "authdata len=%zu challenge len=%zu", sshbuf_len(attestation_cert), 108 sshbuf_len(sig), sshbuf_len(authdata), sshbuf_len(challenge)); 109 110 fido_cred_set_type(cred, credtype); 111 fido_cred_set_fmt(cred, attfmt); 112 fido_cred_set_clientdata(cred, sshbuf_ptr(challenge), 113 sshbuf_len(challenge)); 114 fido_cred_set_rp(cred, rp_id, NULL); 115 fido_cred_set_authdata(cred, sshbuf_ptr(authdata), 116 sshbuf_len(authdata)); 117 /* XXX set_extensions, set_rk, set_uv */ 118 fido_cred_set_x509(cred, sshbuf_ptr(attestation_cert), 119 sshbuf_len(attestation_cert)); 120 fido_cred_set_sig(cred, sshbuf_ptr(sig), sshbuf_len(sig)); 121 122 /* success */ 123 *attestation_certp = attestation_cert; 124 attestation_cert = NULL; 125 r = 0; 126 out: 127 free(magic); 128 sshbuf_free(attestation_cert); 129 sshbuf_free(sig); 130 sshbuf_free(authdata); 131 return r; 132 } 133 134 static uint8_t * 135 get_pubkey_from_cred_ecdsa(const fido_cred_t *cred, size_t *pubkey_len) 136 { 137 const uint8_t *ptr; 138 uint8_t *pubkey = NULL, *ret = NULL; 139 BIGNUM *x = NULL, *y = NULL; 140 EC_POINT *q = NULL; 141 EC_GROUP *g = NULL; 142 143 if ((x = BN_new()) == NULL || 144 (y = BN_new()) == NULL || 145 (g = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)) == NULL || 146 (q = EC_POINT_new(g)) == NULL) { 147 error_f("libcrypto setup failed"); 148 goto out; 149 } 150 if ((ptr = fido_cred_pubkey_ptr(cred)) == NULL) { 151 error_f("fido_cred_pubkey_ptr failed"); 152 goto out; 153 } 154 if (fido_cred_pubkey_len(cred) != 64) { 155 error_f("bad fido_cred_pubkey_len %zu", 156 fido_cred_pubkey_len(cred)); 157 goto out; 158 } 159 160 if (BN_bin2bn(ptr, 32, x) == NULL || 161 BN_bin2bn(ptr + 32, 32, y) == NULL) { 162 error_f("BN_bin2bn failed"); 163 goto out; 164 } 165 if (EC_POINT_set_affine_coordinates_GFp(g, q, x, y, NULL) != 1) { 166 error_f("EC_POINT_set_affine_coordinates_GFp failed"); 167 goto out; 168 } 169 *pubkey_len = EC_POINT_point2oct(g, q, 170 POINT_CONVERSION_UNCOMPRESSED, NULL, 0, NULL); 171 if (*pubkey_len == 0 || *pubkey_len > 2048) { 172 error_f("bad pubkey length %zu", *pubkey_len); 173 goto out; 174 } 175 if ((pubkey = malloc(*pubkey_len)) == NULL) { 176 error_f("malloc pubkey failed"); 177 goto out; 178 } 179 if (EC_POINT_point2oct(g, q, POINT_CONVERSION_UNCOMPRESSED, 180 pubkey, *pubkey_len, NULL) == 0) { 181 error_f("EC_POINT_point2oct failed"); 182 goto out; 183 } 184 /* success */ 185 ret = pubkey; 186 pubkey = NULL; 187 out: 188 free(pubkey); 189 EC_POINT_free(q); 190 EC_GROUP_free(g); 191 BN_clear_free(x); 192 BN_clear_free(y); 193 return ret; 194 } 195 196 /* copied from sshsk_ecdsa_assemble() */ 197 static int 198 cred_matches_key_ecdsa(const fido_cred_t *cred, const struct sshkey *k) 199 { 200 struct sshkey *key = NULL; 201 struct sshbuf *b = NULL; 202 EC_KEY *ec = NULL; 203 uint8_t *pubkey = NULL; 204 size_t pubkey_len; 205 int r; 206 207 if ((key = sshkey_new(KEY_ECDSA_SK)) == NULL) { 208 error_f("sshkey_new failed"); 209 r = SSH_ERR_ALLOC_FAIL; 210 goto out; 211 } 212 key->ecdsa_nid = NID_X9_62_prime256v1; 213 if ((key->pkey = EVP_PKEY_new()) == NULL || 214 (ec = EC_KEY_new_by_curve_name(key->ecdsa_nid)) == NULL || 215 (b = sshbuf_new()) == NULL) { 216 error_f("allocation failed"); 217 r = SSH_ERR_ALLOC_FAIL; 218 goto out; 219 } 220 if ((pubkey = get_pubkey_from_cred_ecdsa(cred, &pubkey_len)) == NULL) { 221 error_f("get_pubkey_from_cred_ecdsa failed"); 222 r = SSH_ERR_INVALID_FORMAT; 223 goto out; 224 } 225 if ((r = sshbuf_put_string(b, pubkey, pubkey_len)) != 0) { 226 error_fr(r, "sshbuf_put_string"); 227 goto out; 228 } 229 if ((r = sshbuf_get_eckey(b, ec)) != 0) { 230 error_fr(r, "parse"); 231 r = SSH_ERR_INVALID_FORMAT; 232 goto out; 233 } 234 if (sshkey_ec_validate_public(EC_KEY_get0_group(ec), 235 EC_KEY_get0_public_key(ec)) != 0) { 236 error("Authenticator returned invalid ECDSA key"); 237 r = SSH_ERR_KEY_INVALID_EC_VALUE; 238 goto out; 239 } 240 if (EVP_PKEY_set1_EC_KEY(key->pkey, ec) != 1) { 241 /* XXX assume it is a allocation error */ 242 error_f("allocation failed"); 243 r = SSH_ERR_ALLOC_FAIL; 244 goto out; 245 } 246 key->sk_application = xstrdup(k->sk_application); /* XXX */ 247 if (!sshkey_equal_public(key, k)) { 248 error("sshkey_equal_public failed"); 249 r = SSH_ERR_INVALID_ARGUMENT; 250 goto out; 251 } 252 r = 0; /* success */ 253 out: 254 EC_KEY_free(ec); 255 free(pubkey); 256 sshkey_free(key); 257 sshbuf_free(b); 258 return r; 259 } 260 261 262 /* copied from sshsk_ed25519_assemble() */ 263 static int 264 cred_matches_key_ed25519(const fido_cred_t *cred, const struct sshkey *k) 265 { 266 struct sshkey *key = NULL; 267 const uint8_t *ptr; 268 int r = -1; 269 270 if ((ptr = fido_cred_pubkey_ptr(cred)) == NULL) { 271 error_f("fido_cred_pubkey_ptr failed"); 272 goto out; 273 } 274 if (fido_cred_pubkey_len(cred) != ED25519_PK_SZ) { 275 error_f("bad fido_cred_pubkey_len %zu", 276 fido_cred_pubkey_len(cred)); 277 goto out; 278 } 279 280 if ((key = sshkey_new(KEY_ED25519_SK)) == NULL) { 281 error_f("sshkey_new failed"); 282 r = SSH_ERR_ALLOC_FAIL; 283 goto out; 284 } 285 if ((key->ed25519_pk = malloc(ED25519_PK_SZ)) == NULL) { 286 error_f("malloc failed"); 287 r = SSH_ERR_ALLOC_FAIL; 288 goto out; 289 } 290 memcpy(key->ed25519_pk, ptr, ED25519_PK_SZ); 291 key->sk_application = xstrdup(k->sk_application); /* XXX */ 292 if (!sshkey_equal_public(key, k)) { 293 error("sshkey_equal_public failed"); 294 r = SSH_ERR_INVALID_ARGUMENT; 295 goto out; 296 } 297 r = 0; /* success */ 298 out: 299 sshkey_free(key); 300 return r; 301 } 302 303 static int 304 cred_matches_key(const fido_cred_t *cred, const struct sshkey *k) 305 { 306 switch (sshkey_type_plain(k->type)) { 307 case KEY_ECDSA_SK: 308 switch (k->ecdsa_nid) { 309 case NID_X9_62_prime256v1: 310 return cred_matches_key_ecdsa(cred, k); 311 break; 312 default: 313 fatal("Unsupported ECDSA key size"); 314 } 315 break; 316 case KEY_ED25519_SK: 317 return cred_matches_key_ed25519(cred, k); 318 default: 319 error_f("key type %s not supported", sshkey_type(k)); 320 return -1; 321 } 322 } 323 324 int 325 main(int argc, char **argv) 326 { 327 LogLevel log_level = SYSLOG_LEVEL_INFO; 328 int r, ch, credtype = -1; 329 struct sshkey *k = NULL; 330 struct sshbuf *attestation = NULL, *challenge = NULL; 331 struct sshbuf *attestation_cert = NULL; 332 char *fp; 333 const char *attfmt = "packed", *style = NULL; 334 fido_cred_t *cred = NULL; 335 int write_attestation_cert = 0; 336 extern int optind; 337 /* extern char *optarg; */ 338 339 ERR_load_crypto_strings(); 340 341 sanitise_stdfd(); 342 log_init(__progname, log_level, SYSLOG_FACILITY_AUTH, 1); 343 344 while ((ch = getopt(argc, argv, "UAv")) != -1) { 345 switch (ch) { 346 case 'U': 347 attfmt = "fido-u2f"; 348 break; 349 case 'A': 350 write_attestation_cert = 1; 351 break; 352 case 'v': 353 if (log_level == SYSLOG_LEVEL_ERROR) 354 log_level = SYSLOG_LEVEL_DEBUG1; 355 else if (log_level < SYSLOG_LEVEL_DEBUG3) 356 log_level++; 357 break; 358 default: 359 goto usage; 360 } 361 } 362 log_init(__progname, log_level, SYSLOG_FACILITY_AUTH, 1); 363 argv += optind; 364 argc -= optind; 365 366 if (argc < 3) { 367 usage: 368 fprintf(stderr, "usage: %s [-vAU] " 369 "pubkey challenge attestation-blob\n", __progname); 370 exit(1); 371 } 372 if ((r = sshkey_load_public(argv[0], &k, NULL)) != 0) 373 fatal_r(r, "load key %s", argv[0]); 374 if ((fp = sshkey_fingerprint(k, SSH_FP_HASH_DEFAULT, 375 SSH_FP_DEFAULT)) == NULL) 376 fatal("sshkey_fingerprint failed"); 377 debug2("key %s: %s %s", argv[2], sshkey_type(k), fp); 378 free(fp); 379 if ((r = sshbuf_load_file(argv[1], &challenge)) != 0) 380 fatal_r(r, "load challenge %s", argv[1]); 381 if ((r = sshbuf_load_file(argv[2], &attestation)) != 0) 382 fatal_r(r, "load attestation %s", argv[2]); 383 if ((cred = fido_cred_new()) == NULL) 384 fatal("fido_cred_new failed"); 385 386 switch (sshkey_type_plain(k->type)) { 387 case KEY_ECDSA_SK: 388 switch (k->ecdsa_nid) { 389 case NID_X9_62_prime256v1: 390 credtype = COSE_ES256; 391 break; 392 default: 393 fatal("Unsupported ECDSA key size"); 394 } 395 break; 396 case KEY_ED25519_SK: 397 credtype = COSE_EDDSA; 398 break; 399 default: 400 fatal("unsupported key type %s", sshkey_type(k)); 401 } 402 403 if ((r = prepare_fido_cred(cred, credtype, attfmt, k->sk_application, 404 attestation, challenge, &attestation_cert)) != 0) 405 fatal_r(r, "prepare_fido_cred %s", argv[2]); 406 if (fido_cred_x5c_ptr(cred) != NULL) { 407 debug("basic attestation"); 408 if ((r = fido_cred_verify(cred)) != FIDO_OK) 409 fatal("basic attestation failed"); 410 style = "basic"; 411 } else { 412 debug("self attestation"); 413 if ((r = fido_cred_verify_self(cred)) != FIDO_OK) 414 fatal("self attestation failed"); 415 style = "self"; 416 } 417 if (cred_matches_key(cred, k) != 0) 418 fatal("cred authdata does not match key"); 419 420 fido_cred_free(&cred); 421 422 if (write_attestation_cert) { 423 PEM_write(stdout, "CERTIFICATE", NULL, 424 sshbuf_ptr(attestation_cert), sshbuf_len(attestation_cert)); 425 } 426 sshbuf_free(attestation_cert); 427 428 logit("%s: verified %s attestation", argv[2], style); 429 430 return (0); 431 } 432