0afa8e065e
git-subtree-dir: contrib/libfido2 git-subtree-mainline:d586c978b9
git-subtree-split:a58dee945a
594 lines
14 KiB
C
594 lines
14 KiB
C
/*
|
|
* Copyright (c) 2020 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.
|
|
*/
|
|
|
|
#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"
|
|
|
|
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
|
|
decompress(const struct blob *plaintext, uint64_t origsiz)
|
|
{
|
|
struct blob inflated;
|
|
u_long ilen, plen;
|
|
int ok = -1;
|
|
|
|
memset(&inflated, 0, sizeof(inflated));
|
|
|
|
if (plaintext->len > ULONG_MAX)
|
|
return -1;
|
|
if (origsiz > ULONG_MAX || origsiz > SIZE_MAX)
|
|
return -1;
|
|
plen = (u_long)plaintext->len;
|
|
ilen = (u_long)origsiz;
|
|
inflated.len = (size_t)origsiz;
|
|
if ((inflated.ptr = calloc(1, inflated.len)) == NULL)
|
|
return -1;
|
|
if (uncompress(inflated.ptr, &ilen, plaintext->ptr, plen) != Z_OK ||
|
|
ilen > SIZE_MAX || (size_t)ilen != (size_t)origsiz)
|
|
goto out;
|
|
|
|
ok = 0; /* success */
|
|
out:
|
|
freezero(inflated.ptr, inflated.len);
|
|
|
|
return ok;
|
|
}
|
|
|
|
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);
|
|
}
|