/* $OpenBSD: ssh-verify-attestation.c,v 1.2 2024/12/06 10:37:42 djm Exp $ */ /* * Copyright (c) 2022-2024 Damien Miller * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /* * This is a small program to verify FIDO attestation objects that * ssh-keygen(1) can record when enrolling a FIDO key. It requires that * the attestation object and challenge used when creating the key be * recorded. * * Example usage: * * $ # Generate a random challenge. * $ dd if=/dev/urandom of=key_ecdsa_sk.challenge bs=32 count=1 * $ # Generate a key, record the attestation blob. * $ ssh-keygen -f key_ecdsa_sk -t ecdsa-sk \ * -Ochallenge=key_ecdsa_sk.challenge \ * -Owrite-attestation=key_ecdsa_sk.attest -N '' * $ # Validate the challenge (-A = print attestation CA cert) * $ ./obj/ssh-verify-attestation -A key_ecdsa_sk key_ecdsa_sk.challenge \ * key_ecdsa_sk.attest * * Limitations/TODO: * * 1) It doesn't automatically detect the attestation statement format. It * assumes the "packed" format used by FIDO2 keys. If that doesn't work, * then try using the -U option to select the "fido-u2f" format. * 2) It makes assumptions about RK, UV, etc status of the key/cred. * 3) Probably bugs. * * Thanks to Markus Friedl and Pedro Martelletto for help getting this * working. */ #include "includes.h" #include #include #include #include #include #include #include "xmalloc.h" #include "log.h" #include "sshbuf.h" #include "sshkey.h" #include "authfile.h" #include "ssherr.h" #include "misc.h" #include "digest.h" #include "crypto_api.h" #include #include #include #include #include #include extern char *__progname; #define ATTEST_MAGIC "ssh-sk-attest-v01" static int prepare_fido_cred(fido_cred_t *cred, int credtype, const char *attfmt, const char *rp_id, struct sshbuf *b, const struct sshbuf *challenge, struct sshbuf **attestation_certp) { struct sshbuf *attestation_cert = NULL, *sig = NULL, *authdata = NULL; char *magic = NULL; int r = SSH_ERR_INTERNAL_ERROR; *attestation_certp = NULL; /* Make sure it's the format we're expecting */ if ((r = sshbuf_get_cstring(b, &magic, NULL)) != 0) { error_fr(r, "parse header"); goto out; } if (strcmp(magic, ATTEST_MAGIC) != 0) { error_f("unsupported format"); r = SSH_ERR_INVALID_FORMAT; goto out; } /* Parse the remaining fields */ if ((r = sshbuf_froms(b, &attestation_cert)) != 0 || (r = sshbuf_froms(b, &sig)) != 0 || (r = sshbuf_froms(b, &authdata)) != 0 || (r = sshbuf_get_u32(b, NULL)) != 0 || /* reserved flags */ (r = sshbuf_get_string_direct(b, NULL, NULL)) != 0) { /* reserved */ error_fr(r, "parse body"); goto out; } debug3_f("attestation cert len=%zu, sig len=%zu, " "authdata len=%zu challenge len=%zu", sshbuf_len(attestation_cert), sshbuf_len(sig), sshbuf_len(authdata), sshbuf_len(challenge)); fido_cred_set_type(cred, credtype); fido_cred_set_fmt(cred, attfmt); fido_cred_set_clientdata(cred, sshbuf_ptr(challenge), sshbuf_len(challenge)); fido_cred_set_rp(cred, rp_id, NULL); fido_cred_set_authdata(cred, sshbuf_ptr(authdata), sshbuf_len(authdata)); /* XXX set_extensions, set_rk, set_uv */ fido_cred_set_x509(cred, sshbuf_ptr(attestation_cert), sshbuf_len(attestation_cert)); fido_cred_set_sig(cred, sshbuf_ptr(sig), sshbuf_len(sig)); /* success */ *attestation_certp = attestation_cert; attestation_cert = NULL; r = 0; out: free(magic); sshbuf_free(attestation_cert); sshbuf_free(sig); sshbuf_free(authdata); return r; } static uint8_t * get_pubkey_from_cred_ecdsa(const fido_cred_t *cred, size_t *pubkey_len) { const uint8_t *ptr; uint8_t *pubkey = NULL, *ret = NULL; BIGNUM *x = NULL, *y = NULL; EC_POINT *q = NULL; EC_GROUP *g = NULL; if ((x = BN_new()) == NULL || (y = BN_new()) == NULL || (g = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)) == NULL || (q = EC_POINT_new(g)) == NULL) { error_f("libcrypto setup failed"); goto out; } if ((ptr = fido_cred_pubkey_ptr(cred)) == NULL) { error_f("fido_cred_pubkey_ptr failed"); goto out; } if (fido_cred_pubkey_len(cred) != 64) { error_f("bad fido_cred_pubkey_len %zu", fido_cred_pubkey_len(cred)); goto out; } if (BN_bin2bn(ptr, 32, x) == NULL || BN_bin2bn(ptr + 32, 32, y) == NULL) { error_f("BN_bin2bn failed"); goto out; } if (EC_POINT_set_affine_coordinates_GFp(g, q, x, y, NULL) != 1) { error_f("EC_POINT_set_affine_coordinates_GFp failed"); goto out; } *pubkey_len = EC_POINT_point2oct(g, q, POINT_CONVERSION_UNCOMPRESSED, NULL, 0, NULL); if (*pubkey_len == 0 || *pubkey_len > 2048) { error_f("bad pubkey length %zu", *pubkey_len); goto out; } if ((pubkey = malloc(*pubkey_len)) == NULL) { error_f("malloc pubkey failed"); goto out; } if (EC_POINT_point2oct(g, q, POINT_CONVERSION_UNCOMPRESSED, pubkey, *pubkey_len, NULL) == 0) { error_f("EC_POINT_point2oct failed"); goto out; } /* success */ ret = pubkey; pubkey = NULL; out: free(pubkey); EC_POINT_free(q); EC_GROUP_free(g); BN_clear_free(x); BN_clear_free(y); return ret; } /* copied from sshsk_ecdsa_assemble() */ static int cred_matches_key_ecdsa(const fido_cred_t *cred, const struct sshkey *k) { struct sshkey *key = NULL; struct sshbuf *b = NULL; EC_KEY *ec = NULL; uint8_t *pubkey = NULL; size_t pubkey_len; int r; if ((key = sshkey_new(KEY_ECDSA_SK)) == NULL) { error_f("sshkey_new failed"); r = SSH_ERR_ALLOC_FAIL; goto out; } key->ecdsa_nid = NID_X9_62_prime256v1; if ((key->pkey = EVP_PKEY_new()) == NULL || (ec = EC_KEY_new_by_curve_name(key->ecdsa_nid)) == NULL || (b = sshbuf_new()) == NULL) { error_f("allocation failed"); r = SSH_ERR_ALLOC_FAIL; goto out; } if ((pubkey = get_pubkey_from_cred_ecdsa(cred, &pubkey_len)) == NULL) { error_f("get_pubkey_from_cred_ecdsa failed"); r = SSH_ERR_INVALID_FORMAT; goto out; } if ((r = sshbuf_put_string(b, pubkey, pubkey_len)) != 0) { error_fr(r, "sshbuf_put_string"); goto out; } if ((r = sshbuf_get_eckey(b, ec)) != 0) { error_fr(r, "parse"); r = SSH_ERR_INVALID_FORMAT; goto out; } if (sshkey_ec_validate_public(EC_KEY_get0_group(ec), EC_KEY_get0_public_key(ec)) != 0) { error("Authenticator returned invalid ECDSA key"); r = SSH_ERR_KEY_INVALID_EC_VALUE; goto out; } if (EVP_PKEY_set1_EC_KEY(key->pkey, ec) != 1) { /* XXX assume it is a allocation error */ error_f("allocation failed"); r = SSH_ERR_ALLOC_FAIL; goto out; } key->sk_application = xstrdup(k->sk_application); /* XXX */ if (!sshkey_equal_public(key, k)) { error("sshkey_equal_public failed"); r = SSH_ERR_INVALID_ARGUMENT; goto out; } r = 0; /* success */ out: EC_KEY_free(ec); free(pubkey); sshkey_free(key); sshbuf_free(b); return r; } /* copied from sshsk_ed25519_assemble() */ static int cred_matches_key_ed25519(const fido_cred_t *cred, const struct sshkey *k) { struct sshkey *key = NULL; const uint8_t *ptr; int r = -1; if ((ptr = fido_cred_pubkey_ptr(cred)) == NULL) { error_f("fido_cred_pubkey_ptr failed"); goto out; } if (fido_cred_pubkey_len(cred) != ED25519_PK_SZ) { error_f("bad fido_cred_pubkey_len %zu", fido_cred_pubkey_len(cred)); goto out; } if ((key = sshkey_new(KEY_ED25519_SK)) == NULL) { error_f("sshkey_new failed"); r = SSH_ERR_ALLOC_FAIL; goto out; } if ((key->ed25519_pk = malloc(ED25519_PK_SZ)) == NULL) { error_f("malloc failed"); r = SSH_ERR_ALLOC_FAIL; goto out; } memcpy(key->ed25519_pk, ptr, ED25519_PK_SZ); key->sk_application = xstrdup(k->sk_application); /* XXX */ if (!sshkey_equal_public(key, k)) { error("sshkey_equal_public failed"); r = SSH_ERR_INVALID_ARGUMENT; goto out; } r = 0; /* success */ out: sshkey_free(key); return r; } static int cred_matches_key(const fido_cred_t *cred, const struct sshkey *k) { switch (sshkey_type_plain(k->type)) { case KEY_ECDSA_SK: switch (k->ecdsa_nid) { case NID_X9_62_prime256v1: return cred_matches_key_ecdsa(cred, k); break; default: fatal("Unsupported ECDSA key size"); } break; case KEY_ED25519_SK: return cred_matches_key_ed25519(cred, k); default: error_f("key type %s not supported", sshkey_type(k)); return -1; } } int main(int argc, char **argv) { LogLevel log_level = SYSLOG_LEVEL_INFO; int r, ch, credtype = -1; struct sshkey *k = NULL; struct sshbuf *attestation = NULL, *challenge = NULL; struct sshbuf *attestation_cert = NULL; char *fp; const char *attfmt = "packed", *style = NULL; fido_cred_t *cred = NULL; int write_attestation_cert = 0; extern int optind; /* extern char *optarg; */ ERR_load_crypto_strings(); sanitise_stdfd(); log_init(__progname, log_level, SYSLOG_FACILITY_AUTH, 1); while ((ch = getopt(argc, argv, "UAv")) != -1) { switch (ch) { case 'U': attfmt = "fido-u2f"; break; case 'A': write_attestation_cert = 1; break; case 'v': if (log_level == SYSLOG_LEVEL_ERROR) log_level = SYSLOG_LEVEL_DEBUG1; else if (log_level < SYSLOG_LEVEL_DEBUG3) log_level++; break; default: goto usage; } } log_init(__progname, log_level, SYSLOG_FACILITY_AUTH, 1); argv += optind; argc -= optind; if (argc < 3) { usage: fprintf(stderr, "usage: %s [-vAU] " "pubkey challenge attestation-blob\n", __progname); exit(1); } if ((r = sshkey_load_public(argv[0], &k, NULL)) != 0) fatal_r(r, "load key %s", argv[0]); if ((fp = sshkey_fingerprint(k, SSH_FP_HASH_DEFAULT, SSH_FP_DEFAULT)) == NULL) fatal("sshkey_fingerprint failed"); debug2("key %s: %s %s", argv[2], sshkey_type(k), fp); free(fp); if ((r = sshbuf_load_file(argv[1], &challenge)) != 0) fatal_r(r, "load challenge %s", argv[1]); if ((r = sshbuf_load_file(argv[2], &attestation)) != 0) fatal_r(r, "load attestation %s", argv[2]); if ((cred = fido_cred_new()) == NULL) fatal("fido_cred_new failed"); switch (sshkey_type_plain(k->type)) { case KEY_ECDSA_SK: switch (k->ecdsa_nid) { case NID_X9_62_prime256v1: credtype = COSE_ES256; break; default: fatal("Unsupported ECDSA key size"); } break; case KEY_ED25519_SK: credtype = COSE_EDDSA; break; default: fatal("unsupported key type %s", sshkey_type(k)); } if ((r = prepare_fido_cred(cred, credtype, attfmt, k->sk_application, attestation, challenge, &attestation_cert)) != 0) fatal_r(r, "prepare_fido_cred %s", argv[2]); if (fido_cred_x5c_ptr(cred) != NULL) { debug("basic attestation"); if ((r = fido_cred_verify(cred)) != FIDO_OK) fatal("basic attestation failed"); style = "basic"; } else { debug("self attestation"); if ((r = fido_cred_verify_self(cred)) != FIDO_OK) fatal("self attestation failed"); style = "self"; } if (cred_matches_key(cred, k) != 0) fatal("cred authdata does not match key"); fido_cred_free(&cred); if (write_attestation_cert) { PEM_write(stdout, "CERTIFICATE", NULL, sshbuf_ptr(attestation_cert), sshbuf_len(attestation_cert)); } sshbuf_free(attestation_cert); logit("%s: verified %s attestation", argv[2], style); return (0); }