summaryrefslogtreecommitdiffstats
path: root/tools/largeblob.c
diff options
context:
space:
mode:
Diffstat (limited to 'tools/largeblob.c')
-rw-r--r--tools/largeblob.c618
1 files changed, 618 insertions, 0 deletions
diff --git a/tools/largeblob.c b/tools/largeblob.c
new file mode 100644
index 0000000..78b97ab
--- /dev/null
+++ b/tools/largeblob.c
@@ -0,0 +1,618 @@
+/*
+ * Copyright (c) 2020-2022 Yubico AB. All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file.
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <fido.h>
+#include <fido/credman.h>
+
+#include <cbor.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+#include <zlib.h>
+
+#include "../openbsd-compat/openbsd-compat.h"
+#include "extern.h"
+
+#define BOUND (1024UL * 1024UL)
+
+struct rkmap {
+ fido_credman_rp_t *rp; /* known rps */
+ fido_credman_rk_t **rk; /* rk per rp */
+};
+
+static void
+free_rkmap(struct rkmap *map)
+{
+ if (map->rp != NULL) {
+ for (size_t i = 0; i < fido_credman_rp_count(map->rp); i++)
+ fido_credman_rk_free(&map->rk[i]);
+ fido_credman_rp_free(&map->rp);
+ }
+ free(map->rk);
+}
+
+static int
+map_known_rps(fido_dev_t *dev, const char *path, struct rkmap *map)
+{
+ const char *rp_id;
+ char *pin = NULL;
+ size_t n;
+ int r, ok = -1;
+
+ if ((map->rp = fido_credman_rp_new()) == NULL) {
+ warnx("%s: fido_credman_rp_new", __func__);
+ goto out;
+ }
+ if ((pin = get_pin(path)) == NULL)
+ goto out;
+ if ((r = fido_credman_get_dev_rp(dev, map->rp, pin)) != FIDO_OK) {
+ warnx("fido_credman_get_dev_rp: %s", fido_strerr(r));
+ goto out;
+ }
+ if ((n = fido_credman_rp_count(map->rp)) > UINT8_MAX) {
+ warnx("%s: fido_credman_rp_count > UINT8_MAX", __func__);
+ goto out;
+ }
+ if ((map->rk = calloc(n, sizeof(*map->rk))) == NULL) {
+ warnx("%s: calloc", __func__);
+ goto out;
+ }
+ for (size_t i = 0; i < n; i++) {
+ if ((rp_id = fido_credman_rp_id(map->rp, i)) == NULL) {
+ warnx("%s: fido_credman_rp_id %zu", __func__, i);
+ goto out;
+ }
+ if ((map->rk[i] = fido_credman_rk_new()) == NULL) {
+ warnx("%s: fido_credman_rk_new", __func__);
+ goto out;
+ }
+ if ((r = fido_credman_get_dev_rk(dev, rp_id, map->rk[i],
+ pin)) != FIDO_OK) {
+ warnx("%s: fido_credman_get_dev_rk %s: %s", __func__,
+ rp_id, fido_strerr(r));
+ goto out;
+ }
+ }
+
+ ok = 0;
+out:
+ freezero(pin, PINBUF_LEN);
+
+ return ok;
+}
+
+static int
+lookup_key(const char *path, fido_dev_t *dev, const char *rp_id,
+ const struct blob *cred_id, char **pin, struct blob *key)
+{
+ fido_credman_rk_t *rk = NULL;
+ const fido_cred_t *cred = NULL;
+ size_t i, n;
+ int r, ok = -1;
+
+ if ((rk = fido_credman_rk_new()) == NULL) {
+ warnx("%s: fido_credman_rk_new", __func__);
+ goto out;
+ }
+ if ((r = fido_credman_get_dev_rk(dev, rp_id, rk, *pin)) != FIDO_OK &&
+ *pin == NULL && should_retry_with_pin(dev, r)) {
+ if ((*pin = get_pin(path)) == NULL)
+ goto out;
+ r = fido_credman_get_dev_rk(dev, rp_id, rk, *pin);
+ }
+ if (r != FIDO_OK) {
+ warnx("%s: fido_credman_get_dev_rk: %s", __func__,
+ fido_strerr(r));
+ goto out;
+ }
+ if ((n = fido_credman_rk_count(rk)) == 0) {
+ warnx("%s: rp id not found", __func__);
+ goto out;
+ }
+ if (n == 1 && cred_id->len == 0) {
+ /* use the credential we found */
+ cred = fido_credman_rk(rk, 0);
+ } else {
+ if (cred_id->len == 0) {
+ warnx("%s: multiple credentials found", __func__);
+ goto out;
+ }
+ for (i = 0; i < n; i++) {
+ const fido_cred_t *x = fido_credman_rk(rk, i);
+ if (fido_cred_id_len(x) <= cred_id->len &&
+ !memcmp(fido_cred_id_ptr(x), cred_id->ptr,
+ fido_cred_id_len(x))) {
+ cred = x;
+ break;
+ }
+ }
+ }
+ if (cred == NULL) {
+ warnx("%s: credential not found", __func__);
+ goto out;
+ }
+ if (fido_cred_largeblob_key_ptr(cred) == NULL) {
+ warnx("%s: no associated blob key", __func__);
+ goto out;
+ }
+ key->len = fido_cred_largeblob_key_len(cred);
+ if ((key->ptr = malloc(key->len)) == NULL) {
+ warnx("%s: malloc", __func__);
+ goto out;
+ }
+ memcpy(key->ptr, fido_cred_largeblob_key_ptr(cred), key->len);
+
+ ok = 0;
+out:
+ fido_credman_rk_free(&rk);
+
+ return ok;
+}
+
+static int
+load_key(const char *keyf, const char *cred_id64, const char *rp_id,
+ const char *path, fido_dev_t *dev, char **pin, struct blob *key)
+{
+ struct blob cred_id;
+ FILE *fp;
+ int r;
+
+ memset(&cred_id, 0, sizeof(cred_id));
+
+ if (keyf != NULL) {
+ if (rp_id != NULL || cred_id64 != NULL)
+ usage();
+ fp = open_read(keyf);
+ if ((r = base64_read(fp, key)) < 0)
+ warnx("%s: base64_read %s", __func__, keyf);
+ fclose(fp);
+ return r;
+ }
+ if (rp_id == NULL)
+ usage();
+ if (cred_id64 != NULL && base64_decode(cred_id64, (void *)&cred_id.ptr,
+ &cred_id.len) < 0) {
+ warnx("%s: base64_decode %s", __func__, cred_id64);
+ return -1;
+ }
+ r = lookup_key(path, dev, rp_id, &cred_id, pin, key);
+ free(cred_id.ptr);
+
+ return r;
+}
+
+int
+blob_set(const char *path, const char *keyf, const char *rp_id,
+ const char *cred_id64, const char *blobf)
+{
+ fido_dev_t *dev;
+ struct blob key, blob;
+ char *pin = NULL;
+ int r, ok = 1;
+
+ dev = open_dev(path);
+ memset(&key, 0, sizeof(key));
+ memset(&blob, 0, sizeof(blob));
+
+ if (read_file(blobf, &blob.ptr, &blob.len) < 0 ||
+ load_key(keyf, cred_id64, rp_id, path, dev, &pin, &key) < 0)
+ goto out;
+ if ((r = fido_dev_largeblob_set(dev, key.ptr, key.len, blob.ptr,
+ blob.len, pin)) != FIDO_OK && should_retry_with_pin(dev, r)) {
+ if ((pin = get_pin(path)) == NULL)
+ goto out;
+ r = fido_dev_largeblob_set(dev, key.ptr, key.len, blob.ptr,
+ blob.len, pin);
+ }
+ if (r != FIDO_OK) {
+ warnx("fido_dev_largeblob_set: %s", fido_strerr(r));
+ goto out;
+ }
+
+ ok = 0; /* success */
+out:
+ freezero(key.ptr, key.len);
+ freezero(blob.ptr, blob.len);
+ freezero(pin, PINBUF_LEN);
+
+ fido_dev_close(dev);
+ fido_dev_free(&dev);
+
+ exit(ok);
+}
+
+int
+blob_get(const char *path, const char *keyf, const char *rp_id,
+ const char *cred_id64, const char *blobf)
+{
+ fido_dev_t *dev;
+ struct blob key, blob;
+ char *pin = NULL;
+ int r, ok = 1;
+
+ dev = open_dev(path);
+ memset(&key, 0, sizeof(key));
+ memset(&blob, 0, sizeof(blob));
+
+ if (load_key(keyf, cred_id64, rp_id, path, dev, &pin, &key) < 0)
+ goto out;
+ if ((r = fido_dev_largeblob_get(dev, key.ptr, key.len, &blob.ptr,
+ &blob.len)) != FIDO_OK) {
+ warnx("fido_dev_largeblob_get: %s", fido_strerr(r));
+ goto out;
+ }
+ if (write_file(blobf, blob.ptr, blob.len) < 0)
+ goto out;
+
+ ok = 0; /* success */
+out:
+ freezero(key.ptr, key.len);
+ freezero(blob.ptr, blob.len);
+ freezero(pin, PINBUF_LEN);
+
+ fido_dev_close(dev);
+ fido_dev_free(&dev);
+
+ exit(ok);
+}
+
+int
+blob_delete(const char *path, const char *keyf, const char *rp_id,
+ const char *cred_id64)
+{
+ fido_dev_t *dev;
+ struct blob key;
+ char *pin = NULL;
+ int r, ok = 1;
+
+ dev = open_dev(path);
+ memset(&key, 0, sizeof(key));
+
+ if (load_key(keyf, cred_id64, rp_id, path, dev, &pin, &key) < 0)
+ goto out;
+ if ((r = fido_dev_largeblob_remove(dev, key.ptr, key.len,
+ pin)) != FIDO_OK && should_retry_with_pin(dev, r)) {
+ if ((pin = get_pin(path)) == NULL)
+ goto out;
+ r = fido_dev_largeblob_remove(dev, key.ptr, key.len, pin);
+ }
+ if (r != FIDO_OK) {
+ warnx("fido_dev_largeblob_remove: %s", fido_strerr(r));
+ goto out;
+ }
+
+ ok = 0; /* success */
+out:
+ freezero(key.ptr, key.len);
+ freezero(pin, PINBUF_LEN);
+
+ fido_dev_close(dev);
+ fido_dev_free(&dev);
+
+ exit(ok);
+}
+
+static int
+try_decompress(const struct blob *in, uint64_t origsiz, int wbits)
+{
+ struct blob out;
+ z_stream zs;
+ u_int ilen, olen;
+ int ok = -1;
+
+ memset(&zs, 0, sizeof(zs));
+ memset(&out, 0, sizeof(out));
+
+ if (in->len > UINT_MAX || (ilen = (u_int)in->len) > BOUND)
+ return -1;
+ if (origsiz > SIZE_MAX || origsiz > UINT_MAX ||
+ (olen = (u_int)origsiz) > BOUND)
+ return -1;
+ if (inflateInit2(&zs, wbits) != Z_OK)
+ return -1;
+
+ if ((out.ptr = calloc(1, olen)) == NULL)
+ goto fail;
+
+ out.len = olen;
+ zs.next_in = in->ptr;
+ zs.avail_in = ilen;
+ zs.next_out = out.ptr;
+ zs.avail_out = olen;
+
+ if (inflate(&zs, Z_FINISH) != Z_STREAM_END)
+ goto fail;
+ if (zs.avail_out != 0)
+ goto fail;
+
+ ok = 0;
+fail:
+ if (inflateEnd(&zs) != Z_OK)
+ ok = -1;
+
+ freezero(out.ptr, out.len);
+
+ return ok;
+}
+
+static int
+decompress(const struct blob *plaintext, uint64_t origsiz)
+{
+ if (try_decompress(plaintext, origsiz, MAX_WBITS) == 0) /* rfc1950 */
+ return 0;
+ return try_decompress(plaintext, origsiz, -MAX_WBITS); /* rfc1951 */
+}
+
+static int
+decode(const struct blob *ciphertext, const struct blob *nonce,
+ uint64_t origsiz, const fido_cred_t *cred)
+{
+ uint8_t aad[4 + sizeof(uint64_t)];
+ EVP_CIPHER_CTX *ctx = NULL;
+ const EVP_CIPHER *cipher;
+ struct blob plaintext;
+ uint64_t tmp;
+ int ok = -1;
+
+ memset(&plaintext, 0, sizeof(plaintext));
+
+ if (nonce->len != 12)
+ return -1;
+ if (cred == NULL ||
+ fido_cred_largeblob_key_ptr(cred) == NULL ||
+ fido_cred_largeblob_key_len(cred) != 32)
+ return -1;
+ if (ciphertext->len > UINT_MAX ||
+ ciphertext->len > SIZE_MAX - 16 ||
+ ciphertext->len < 16)
+ return -1;
+ plaintext.len = ciphertext->len - 16;
+ if ((plaintext.ptr = calloc(1, plaintext.len)) == NULL)
+ return -1;
+ if ((ctx = EVP_CIPHER_CTX_new()) == NULL ||
+ (cipher = EVP_aes_256_gcm()) == NULL ||
+ EVP_CipherInit(ctx, cipher, fido_cred_largeblob_key_ptr(cred),
+ nonce->ptr, 0) == 0)
+ goto out;
+ if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16,
+ ciphertext->ptr + ciphertext->len - 16) == 0)
+ goto out;
+ aad[0] = 0x62; /* b */
+ aad[1] = 0x6c; /* l */
+ aad[2] = 0x6f; /* o */
+ aad[3] = 0x62; /* b */
+ tmp = htole64(origsiz);
+ memcpy(&aad[4], &tmp, sizeof(uint64_t));
+ if (EVP_Cipher(ctx, NULL, aad, (u_int)sizeof(aad)) < 0 ||
+ EVP_Cipher(ctx, plaintext.ptr, ciphertext->ptr,
+ (u_int)plaintext.len) < 0 ||
+ EVP_Cipher(ctx, NULL, NULL, 0) < 0)
+ goto out;
+ if (decompress(&plaintext, origsiz) < 0)
+ goto out;
+
+ ok = 0;
+out:
+ freezero(plaintext.ptr, plaintext.len);
+
+ if (ctx != NULL)
+ EVP_CIPHER_CTX_free(ctx);
+
+ return ok;
+}
+
+static const fido_cred_t *
+try_rp(const fido_credman_rk_t *rk, const struct blob *ciphertext,
+ const struct blob *nonce, uint64_t origsiz)
+{
+ const fido_cred_t *cred;
+
+ for (size_t i = 0; i < fido_credman_rk_count(rk); i++)
+ if ((cred = fido_credman_rk(rk, i)) != NULL &&
+ decode(ciphertext, nonce, origsiz, cred) == 0)
+ return cred;
+
+ return NULL;
+}
+
+static int
+decode_cbor_blob(struct blob *out, const cbor_item_t *item)
+{
+ if (out->ptr != NULL ||
+ cbor_isa_bytestring(item) == false ||
+ cbor_bytestring_is_definite(item) == false)
+ return -1;
+ out->len = cbor_bytestring_length(item);
+ if ((out->ptr = malloc(out->len)) == NULL)
+ return -1;
+ memcpy(out->ptr, cbor_bytestring_handle(item), out->len);
+
+ return 0;
+}
+
+static int
+decode_blob_entry(const cbor_item_t *item, struct blob *ciphertext,
+ struct blob *nonce, uint64_t *origsiz)
+{
+ struct cbor_pair *v;
+
+ if (item == NULL)
+ return -1;
+ if (cbor_isa_map(item) == false ||
+ cbor_map_is_definite(item) == false ||
+ (v = cbor_map_handle(item)) == NULL)
+ return -1;
+ if (cbor_map_size(item) > UINT8_MAX)
+ return -1;
+
+ for (size_t i = 0; i < cbor_map_size(item); i++) {
+ if (cbor_isa_uint(v[i].key) == false ||
+ cbor_int_get_width(v[i].key) != CBOR_INT_8)
+ continue; /* ignore */
+ switch (cbor_get_uint8(v[i].key)) {
+ case 1: /* ciphertext */
+ if (decode_cbor_blob(ciphertext, v[i].value) < 0)
+ return -1;
+ break;
+ case 2: /* nonce */
+ if (decode_cbor_blob(nonce, v[i].value) < 0)
+ return -1;
+ break;
+ case 3: /* origSize */
+ if (*origsiz != 0 ||
+ cbor_isa_uint(v[i].value) == false ||
+ (*origsiz = cbor_get_int(v[i].value)) > SIZE_MAX)
+ return -1;
+ }
+ }
+ if (ciphertext->ptr == NULL || nonce->ptr == NULL || *origsiz == 0)
+ return -1;
+
+ return 0;
+}
+
+static void
+print_blob_entry(size_t idx, const cbor_item_t *item, const struct rkmap *map)
+{
+ struct blob ciphertext, nonce;
+ const fido_cred_t *cred = NULL;
+ const char *rp_id = NULL;
+ char *cred_id = NULL;
+ uint64_t origsiz = 0;
+
+ memset(&ciphertext, 0, sizeof(ciphertext));
+ memset(&nonce, 0, sizeof(nonce));
+
+ if (decode_blob_entry(item, &ciphertext, &nonce, &origsiz) < 0) {
+ printf("%02zu: <skipped: bad cbor>\n", idx);
+ goto out;
+ }
+ for (size_t i = 0; i < fido_credman_rp_count(map->rp); i++) {
+ if ((cred = try_rp(map->rk[i], &ciphertext, &nonce,
+ origsiz)) != NULL) {
+ rp_id = fido_credman_rp_id(map->rp, i);
+ break;
+ }
+ }
+ if (cred == NULL) {
+ if ((cred_id = strdup("<unknown>")) == NULL) {
+ printf("%02zu: <skipped: strdup failed>\n", idx);
+ goto out;
+ }
+ } else {
+ if (base64_encode(fido_cred_id_ptr(cred),
+ fido_cred_id_len(cred), &cred_id) < 0) {
+ printf("%02zu: <skipped: base64_encode failed>\n", idx);
+ goto out;
+ }
+ }
+ if (rp_id == NULL)
+ rp_id = "<unknown>";
+
+ printf("%02zu: %4zu %4zu %s %s\n", idx, ciphertext.len,
+ (size_t)origsiz, cred_id, rp_id);
+out:
+ free(ciphertext.ptr);
+ free(nonce.ptr);
+ free(cred_id);
+}
+
+static cbor_item_t *
+get_cbor_array(fido_dev_t *dev)
+{
+ struct cbor_load_result cbor_result;
+ cbor_item_t *item = NULL;
+ u_char *cbor_ptr = NULL;
+ size_t cbor_len;
+ int r, ok = -1;
+
+ if ((r = fido_dev_largeblob_get_array(dev, &cbor_ptr,
+ &cbor_len)) != FIDO_OK) {
+ warnx("%s: fido_dev_largeblob_get_array: %s", __func__,
+ fido_strerr(r));
+ goto out;
+ }
+ if ((item = cbor_load(cbor_ptr, cbor_len, &cbor_result)) == NULL) {
+ warnx("%s: cbor_load", __func__);
+ goto out;
+ }
+ if (cbor_result.read != cbor_len) {
+ warnx("%s: cbor_result.read (%zu) != cbor_len (%zu)", __func__,
+ cbor_result.read, cbor_len);
+ /* continue */
+ }
+ if (cbor_isa_array(item) == false ||
+ cbor_array_is_definite(item) == false) {
+ warnx("%s: cbor type", __func__);
+ goto out;
+ }
+ if (cbor_array_size(item) > UINT8_MAX) {
+ warnx("%s: cbor_array_size > UINT8_MAX", __func__);
+ goto out;
+ }
+ if (cbor_array_size(item) == 0) {
+ ok = 0; /* nothing to do */
+ goto out;
+ }
+
+ printf("total map size: %zu byte%s\n", cbor_len, plural(cbor_len));
+
+ ok = 0;
+out:
+ if (ok < 0 && item != NULL) {
+ cbor_decref(&item);
+ item = NULL;
+ }
+ free(cbor_ptr);
+
+ return item;
+}
+
+int
+blob_list(const char *path)
+{
+ struct rkmap map;
+ fido_dev_t *dev = NULL;
+ cbor_item_t *item = NULL, **v;
+ int ok = 1;
+
+ memset(&map, 0, sizeof(map));
+ dev = open_dev(path);
+ if (map_known_rps(dev, path, &map) < 0 ||
+ (item = get_cbor_array(dev)) == NULL)
+ goto out;
+ if (cbor_array_size(item) == 0) {
+ ok = 0; /* nothing to do */
+ goto out;
+ }
+ if ((v = cbor_array_handle(item)) == NULL) {
+ warnx("%s: cbor_array_handle", __func__);
+ goto out;
+ }
+ for (size_t i = 0; i < cbor_array_size(item); i++)
+ print_blob_entry(i, v[i], &map);
+
+ ok = 0; /* success */
+out:
+ free_rkmap(&map);
+
+ if (item != NULL)
+ cbor_decref(&item);
+
+ fido_dev_close(dev);
+ fido_dev_free(&dev);
+
+ exit(ok);
+}