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