/*
* Copyright (C) 2014-2022 Yubico AB - See COPYING
*/
#include <fido.h>
#include <fido/es256.h>
#include <fido/rs256.h>
#include <fido/eddsa.h>
#include <openssl/ec.h>
#include <openssl/obj_mac.h>
#include <inttypes.h>
#include <limits.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <pwd.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include "b64.h"
#include "debug.h"
#include "util.h"
#define SSH_MAX_SIZE 8192
#define SSH_HEADER "-----BEGIN OPENSSH PRIVATE KEY-----\n"
#define SSH_HEADER_LEN (sizeof(SSH_HEADER) - 1)
#define SSH_TRAILER "-----END OPENSSH PRIVATE KEY-----\n"
#define SSH_TRAILER_LEN (sizeof(SSH_TRAILER) - 1)
#define SSH_AUTH_MAGIC "openssh-key-v1"
#define SSH_AUTH_MAGIC_LEN (sizeof(SSH_AUTH_MAGIC)) // AUTH_MAGIC includes \0
#define SSH_ES256 "
[email protected]"
#define SSH_ES256_LEN (sizeof(SSH_ES256) - 1)
#define SSH_ES256_POINT_LEN 65
#define SSH_P256_NAME "nistp256"
#define SSH_P256_NAME_LEN (sizeof(SSH_P256_NAME) - 1)
#define SSH_EDDSA "
[email protected]"
#define SSH_EDDSA_LEN (sizeof(SSH_EDDSA) - 1)
#define SSH_EDDSA_POINT_LEN 32
#define SSH_SK_USER_PRESENCE_REQD 0x01
#define SSH_SK_USER_VERIFICATION_REQD 0x04
#define SSH_SK_RESIDENT_KEY 0x20
struct opts {
fido_opt_t up;
fido_opt_t uv;
fido_opt_t pin;
};
struct pk {
void *ptr;
int type;
};
static int hex_decode(const char *ascii_hex, unsigned char **blob,
size_t *blob_len) {
*blob = NULL;
*blob_len = 0;
if (ascii_hex == NULL || (strlen(ascii_hex) % 2) != 0)
return (0);
*blob_len = strlen(ascii_hex) / 2;
*blob = calloc(1, *blob_len);
if (*blob == NULL)
return (0);
for (size_t i = 0; i < *blob_len; i++) {
unsigned int c;
int n = -1;
int r = sscanf(ascii_hex, "%02x%n", &c, &n);
if (r != 1 || n != 2 || c > UCHAR_MAX) {
free(*blob);
*blob = NULL;
*blob_len = 0;
return (0);
}
(*blob)[i] = (unsigned char) c;
ascii_hex += n;
}
return (1);
}
static char *normal_b64(const char *websafe_b64) {
char *b64;
char *p;
size_t n;
n = strlen(websafe_b64);
if (n > SIZE_MAX - 3)
return (NULL);
b64 = calloc(1, n + 3);
if (b64 == NULL)
return (NULL);
memcpy(b64, websafe_b64, n);
p = b64;
while ((p = strpbrk(p, "-_")) != NULL) {
switch (*p) {
case '-':
*p++ = '+';
break;
case '_':
*p++ = '/';
break;
}
}
switch (n % 4) {
case 1:
b64[n] = '=';
break;
case 2:
case 3:
b64[n] = '=';
b64[n + 1] = '=';
break;
}
return (b64);
}
static int translate_old_format_pubkey(es256_pk_t *es256_pk,
const unsigned char *pk, size_t pk_len) {
EC_KEY *ec = NULL;
EC_POINT *q = NULL;
const EC_GROUP *g = NULL;
int r = FIDO_ERR_INTERNAL;
if (es256_pk == NULL)
goto fail;
if ((ec = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)) == NULL ||
(g = EC_KEY_get0_group(ec)) == NULL)
goto fail;
if ((q = EC_POINT_new(g)) == NULL ||
!EC_POINT_oct2point(g, q, pk, pk_len, NULL) ||
!EC_KEY_set_public_key(ec, q))
goto fail;
r = es256_pk_from_EC_KEY(es256_pk, ec);
fail:
if (ec != NULL)
EC_KEY_free(ec);
if (q != NULL)
EC_POINT_free(q);
return r;
}
static int is_resident(const char *kh) { return strcmp(kh, "*") == 0; }
static void reset_device(device_t *device) {
free(device->keyHandle);
free(device->publicKey);
free(device->coseType);
free(device->attributes);
memset(device, 0, sizeof(*device));
}
static int parse_native_credential(const cfg_t *cfg, char *s, device_t *cred) {
const char *delim = ",";
const char *kh, *pk, *type, *attr;
char *saveptr = NULL;
memset(cred, 0, sizeof(*cred));
if ((kh = strtok_r(s, delim, &saveptr)) == NULL) {
debug_dbg(cfg, "Missing key handle");
goto fail;
}
if ((pk = strtok_r(NULL, delim, &saveptr)) == NULL) {
debug_dbg(cfg, "Missing public key");
goto fail;
}
if ((type = strtok_r(NULL, delim, &saveptr)) == NULL) {
debug_dbg(cfg, "Old format, assume es256 and +presence");
cred->old_format = 1;
type = "es256";
attr = "+presence";
} else if ((attr = strtok_r(NULL, delim, &saveptr)) == NULL) {
debug_dbg(cfg, "Empty attributes");
attr = "";
}
cred->keyHandle = cred->old_format ? normal_b64(kh) : strdup(kh);
if (cred->keyHandle == NULL || (cred->publicKey = strdup(pk)) == NULL ||
(cred->coseType = strdup(type)) == NULL ||
(cred->attributes = strdup(attr)) == NULL) {
debug_dbg(cfg, "Unable to allocate memory for credential components");
goto fail;
}
return 1;
fail:
reset_device(cred);
return 0;
}
static int parse_native_format(const cfg_t *cfg, const char *username,
FILE *opwfile, device_t *devices,
unsigned *n_devs) {
char *s_user, *s_credential;
char *buf = NULL;
size_t bufsiz = 0;
ssize_t len;
unsigned i;
int r = 0;
while ((len = getline(&buf, &bufsiz, opwfile)) != -1) {
char *saveptr = NULL;
if (len > 0 && buf[len - 1] == '\n')
buf[len - 1] = '\0';
debug_dbg(cfg, "Read %zu bytes", len);
s_user = strtok_r(buf, ":", &saveptr);
if (s_user && strcmp(username, s_user) == 0) {
debug_dbg(cfg, "Matched user: %s", s_user);
// only keep last line for this user
for (i = 0; i < *n_devs; i++) {
reset_device(&devices[i]);
}
*n_devs = 0;
i = 0;
while ((s_credential = strtok_r(NULL, ":", &saveptr))) {
if ((*n_devs)++ > cfg->max_devs - 1) {
*n_devs = cfg->max_devs;
debug_dbg(cfg,
"Found more than %d devices, ignoring the remaining ones",
cfg->max_devs);
break;
}
if (!parse_native_credential(cfg, s_credential, &devices[i])) {
debug_dbg(cfg, "Failed to parse credential");
goto fail;
}
debug_dbg(cfg, "KeyHandle for device number %u: %s", i + 1,
devices[i].keyHandle);
debug_dbg(cfg, "publicKey for device number %u: %s", i + 1,
devices[i].publicKey);
debug_dbg(cfg, "COSE type for device number %u: %s", i + 1,
devices[i].coseType);
debug_dbg(cfg, "Attributes for device number %u: %s", i + 1,
devices[i].attributes);
i++;
}
}
}
if (!feof(opwfile)) {
debug_dbg(cfg, "authfile parsing ended before eof (%d)", errno);
goto fail;
}
r = 1;
fail:
free(buf);
return r;
}
static int load_ssh_key(const cfg_t *cfg, char **out, FILE *opwfile,
size_t opwfile_size) {
size_t buf_size;
char *buf = NULL;
char *cp = NULL;
int r = 0;
int ch;
*out = NULL;
if (opwfile_size < SSH_HEADER_LEN + SSH_TRAILER_LEN) {
debug_dbg(cfg, "Malformed SSH key (length)");
goto fail;
}
buf_size = opwfile_size > SSH_MAX_SIZE ? SSH_MAX_SIZE : opwfile_size;
if ((cp = buf = calloc(1, buf_size)) == NULL) {
debug_dbg(cfg, "Failed to allocate buffer for SSH key");
goto fail;
}
// NOTE(adma): +1 for \0
if (fgets(buf, (int)(SSH_HEADER_LEN + 1), opwfile) == NULL ||
strlen(buf) != SSH_HEADER_LEN ||
strncmp(buf, SSH_HEADER, SSH_HEADER_LEN) != 0) {
debug_dbg(cfg, "Malformed SSH key (header)");
goto fail;
}
while (opwfile_size > 0 && buf_size > 1) {
ch = fgetc(opwfile);
if (ch == EOF) {
debug_dbg(cfg, "Unexpected authfile termination");
goto fail;
}
opwfile_size--;
if (ch != '\n' && ch != '\r') {
*cp = (char) ch;
buf_size--;
if (ch == '-') {
// NOTE(adma): no +1 here since we already read one '-'
if (buf_size < SSH_TRAILER_LEN ||
fgets(cp + 1, (int)SSH_TRAILER_LEN, opwfile) == NULL ||
strlen(cp) != SSH_TRAILER_LEN ||
strncmp(cp, SSH_TRAILER, SSH_TRAILER_LEN) != 0) {
debug_dbg(cfg, "Malformed SSH key (trailer)");
goto fail;
}
r = 1;
*(cp) = '\0';
break;
} else {
cp++;
}
}
}
fail:
if (r != 1) {
free(buf);
buf = NULL;
}
*out = buf;
return r;
}
static int ssh_get(const unsigned char **buf, size_t *size, unsigned char *dst,
size_t len) {
if (*size < len)
return 0;
if (dst != NULL)
memcpy(dst, *buf, len);
*buf += len;
*size -= len;
return 1;
}
static int ssh_get_u8(const unsigned char **buf, size_t *size, uint8_t *val) {
return ssh_get(buf, size, val, sizeof(*val));
}
static int ssh_get_u32(const unsigned char **buf, size_t *size, uint32_t *val) {
if (!ssh_get(buf, size, (unsigned char *) val, sizeof(*val)))
return 0;
if (val != NULL)
*val = ntohl(*val);
return 1;
}
static int ssh_get_string_ref(const unsigned char **buf, size_t *size,
const unsigned char **ref, size_t *lenp) {
uint32_t len;
if (!ssh_get_u32(buf, size, &len))
return 0;
if (!ssh_get(buf, size, NULL, len))
return 0;
if (ref != NULL)
*ref = *buf - len;
if (lenp != NULL)
*lenp = len;
return 1;
}
static int ssh_get_cstring(const unsigned char **buf, size_t *size, char **str,
size_t *lenp) {
const unsigned char *ref;
size_t len;
if (!ssh_get_string_ref(buf, size, &ref, &len))
return 0;
if (str != NULL) {
if (len > SIZE_MAX - 1 || (*str = calloc(1, len + 1)) == NULL)
return 0;
memcpy(*str, ref, len);
}
if (lenp != NULL)
*lenp = len;
return 1;
}
static int ssh_log_cstring(const cfg_t *cfg, const unsigned char **buf,
size_t *size, const char *name) {
char *str = NULL;
size_t len;
(void) name; // silence compiler warnings if PAM_DEBUG disabled
if (!ssh_get_cstring(buf, size, &str, &len)) {
debug_dbg(cfg, "Malformed SSH key (%s)", name);
return 0;
}
debug_dbg(cfg, "%s (%zu) \"%s\"", name, len, str);
free(str);
return 1;
}
static int ssh_get_attrs(const cfg_t *cfg, const unsigned char **buf,
size_t *size, char **attrs) {
char tmp[32] = {0};
uint8_t flags;
int r;
// flags
if (!ssh_get_u8(buf, size, &flags)) {
debug_dbg(cfg, "Malformed SSH key (flags)");
return 0;
}
debug_dbg(cfg, "flags: %02x", flags);
r = snprintf(tmp, sizeof(tmp), "%s%s",
flags & SSH_SK_USER_PRESENCE_REQD ? "+presence" : "",
flags & SSH_SK_USER_VERIFICATION_REQD ? "+verification" : "");
if (r < 0 || (size_t) r >= sizeof(tmp)) {
debug_dbg(cfg, "Unable to prepare flags");
return 0;
}
if ((*attrs = strdup(tmp)) == NULL) {
debug_dbg(cfg, "Unable to allocate attributes");
return 0;
}
return 1;
}
static int ssh_get_pubkey(const cfg_t *cfg, const unsigned char **buf,
size_t *size, char **type_p, char **pubkey_p) {
char *ssh_type = NULL;
char *ssh_curve = NULL;
const unsigned char *blob;
size_t len;
int type;
size_t point_len;
int ok = 0;
*type_p = NULL;
*pubkey_p = NULL;
// key type
if (!ssh_get_cstring(buf, size, &ssh_type, &len)) {
debug_dbg(cfg, "Malformed SSH key (keytype)");
goto err;
}
if (len == SSH_ES256_LEN && memcmp(ssh_type, SSH_ES256, SSH_ES256_LEN) == 0) {
type = COSE_ES256;
point_len = SSH_ES256_POINT_LEN;
} else if (len == SSH_EDDSA_LEN &&
memcmp(ssh_type, SSH_EDDSA, SSH_EDDSA_LEN) == 0) {
type = COSE_EDDSA;
point_len = SSH_EDDSA_POINT_LEN;
} else {
debug_dbg(cfg, "Unknown key type %s", ssh_type);
goto err;
}
debug_dbg(cfg, "keytype (%zu) \"%s\"", len, ssh_type);
if (type == COSE_ES256) {
// curve name
if (!ssh_get_cstring(buf, size, &ssh_curve, &len)) {
debug_dbg(cfg, "Malformed SSH key (curvename)");
goto err;
}
if (len == SSH_P256_NAME_LEN &&
memcmp(ssh_curve, SSH_P256_NAME, SSH_P256_NAME_LEN) == 0) {
debug_dbg(cfg, "curvename (%zu) \"%s\"", len, ssh_curve);
} else {
debug_dbg(cfg, "Unknown curve %s", ssh_curve);
goto err;
}
}
// point
if (!ssh_get_string_ref(buf, size, &blob, &len)) {
debug_dbg(cfg, "Malformed SSH key (point)");
goto err;
}
if (len != point_len) {
debug_dbg(cfg, "Invalid point length, should be %zu, found %zu", point_len,
len);
goto err;
}
if (type == COSE_ES256) {
// Skip the initial '04'
if (len < 1) {
debug_dbg(cfg, "Failed to skip initial '04'");
goto err;
}
blob++;
len--;
}
if (!b64_encode(blob, len, pubkey_p)) {
debug_dbg(cfg, "Unable to allocate public key");
goto err;
}
if ((*type_p = strdup(cose_string(type))) == NULL) {
debug_dbg(cfg, "Unable to allocate COSE type");
goto err;
}
ok = 1;
err:
if (!ok) {
free(*type_p);
free(*pubkey_p);
*type_p = NULL;
*pubkey_p = NULL;
}
free(ssh_type);
free(ssh_curve);
return ok;
}
static int parse_ssh_format(const cfg_t *cfg, FILE *opwfile,
size_t opwfile_size, device_t *devices,
unsigned *n_devs) {
char *b64 = NULL;
const unsigned char *decoded;
unsigned char *decoded_initial = NULL;
size_t decoded_len;
const unsigned char *blob;
uint32_t check1, check2, tmp;
size_t len;
int r = 0;
// The logic below is inspired by
// how ssh parses its own keys. See sshkey.c
reset_device(&devices[0]);
*n_devs = 0;
if (!load_ssh_key(cfg, &b64, opwfile, opwfile_size) ||
!b64_decode(b64, (void **) &decoded_initial, &decoded_len)) {
debug_dbg(cfg, "Unable to decode credential");
goto out;
}
decoded = decoded_initial;
// magic
if (decoded_len < SSH_AUTH_MAGIC_LEN ||
memcmp(decoded, SSH_AUTH_MAGIC, SSH_AUTH_MAGIC_LEN) != 0) {
debug_dbg(cfg, "Malformed SSH key (magic)");
goto out;
}
decoded += SSH_AUTH_MAGIC_LEN;
decoded_len -= SSH_AUTH_MAGIC_LEN;
if (!ssh_log_cstring(cfg, &decoded, &decoded_len, "ciphername") ||
!ssh_log_cstring(cfg, &decoded, &decoded_len, "kdfname") ||
!ssh_log_cstring(cfg, &decoded, &decoded_len, "kdfoptions"))
goto out;
if (!ssh_get_u32(&decoded, &decoded_len, &tmp)) {
debug_dbg(cfg, "Malformed SSH key (nkeys)");
goto out;
}
debug_dbg(cfg, "nkeys: %" PRIu32, tmp);
if (tmp != 1) {
debug_dbg(cfg, "Multiple keys not supported");
goto out;
}
// public_key (skip)
if (!ssh_get_string_ref(&decoded, &decoded_len, NULL, NULL)) {
debug_dbg(cfg, "Malformed SSH key (pubkey)");
goto out;
}
// private key (consume length)
if (!ssh_get_u32(&decoded, &decoded_len, &tmp) || decoded_len < tmp) {
debug_dbg(cfg, "Malformed SSH key (pvtkey length)");
goto out;
}
// check1, check2
if (!ssh_get_u32(&decoded, &decoded_len, &check1) ||
!ssh_get_u32(&decoded, &decoded_len, &check2)) {
debug_dbg(cfg, "Malformed SSH key (check1, check2)");
goto out;
}
debug_dbg(cfg, "check1: %" PRIu32, check1);
debug_dbg(cfg, "check2: %" PRIu32, check2);
if (check1 != check2) {
debug_dbg(cfg, "Mismatched check values");
goto out;
}
if (!ssh_get_pubkey(cfg, &decoded, &decoded_len, &devices[0].coseType,
&devices[0].publicKey) ||
!ssh_log_cstring(cfg, &decoded, &decoded_len, "application") ||
!ssh_get_attrs(cfg, &decoded, &decoded_len, &devices[0].attributes))
goto out;
// keyhandle
if (!ssh_get_string_ref(&decoded, &decoded_len, &blob, &len) ||
!b64_encode(blob, len, &devices[0].keyHandle)) {
debug_dbg(cfg, "Malformed SSH key (keyhandle)");
goto out;
}
debug_dbg(cfg, "KeyHandle for device number 1: %s", devices[0].keyHandle);
debug_dbg(cfg, "publicKey for device number 1: %s", devices[0].publicKey);
debug_dbg(cfg, "COSE type for device number 1: %s", devices[0].coseType);
debug_dbg(cfg, "Attributes for device number 1: %s", devices[0].attributes);
// reserved (skip)
if (!ssh_get_string_ref(&decoded, &decoded_len, NULL, NULL)) {
debug_dbg(cfg, "Malformed SSH key (reserved)");
goto out;
}
// comment
if (!ssh_log_cstring(cfg, &decoded, &decoded_len, "comment"))
goto out;
// padding
if (decoded_len >= 255) {
debug_dbg(cfg, "Malformed SSH key (padding length)");
goto out;
}
for (int i = 1; (unsigned) i <= decoded_len; i++) {
if (decoded[i - 1] != i) {
debug_dbg(cfg, "Malformed SSH key (padding)");
goto out;
}
}
*n_devs = 1;
r = 1;
out:
if (r != 1) {
reset_device(&devices[0]);
*n_devs = 0;
}
free(decoded_initial);
free(b64);
return r;
}
int get_devices_from_authfile(const cfg_t *cfg, const char *username,
device_t *devices, unsigned *n_devs) {
int r = PAM_AUTHINFO_UNAVAIL;
int fd = -1;
struct stat st;
struct passwd *pw = NULL, pw_s;
char buffer[BUFSIZE];
int gpu_ret;
FILE *opwfile = NULL;
size_t opwfile_size;
unsigned i;
/* Ensure we never return uninitialized count. */
*n_devs = 0;
fd = open(cfg->auth_file, O_RDONLY | O_CLOEXEC | O_NOCTTY);
if (fd < 0) {
if (errno == ENOENT && cfg->nouserok) {
r = PAM_IGNORE;
}
debug_dbg(cfg, "Cannot open authentication file: %s", strerror(errno));
goto err;
}
if (fstat(fd, &st) < 0) {
debug_dbg(cfg, "Cannot stat authentication file: %s", strerror(errno));
goto err;
}
if (!S_ISREG(st.st_mode)) {
debug_dbg(cfg, "Authentication file is not a regular file");
goto err;
}
if ((st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
debug_dbg(cfg, "Authentication file has insecure permissions");
goto err;
}
opwfile_size = (size_t)st.st_size;
gpu_ret = getpwuid_r(st.st_uid, &pw_s, buffer, sizeof(buffer), &pw);
if (gpu_ret != 0 || pw == NULL) {
debug_dbg(cfg, "Unable to retrieve credentials for uid %u, (%s)", st.st_uid,
strerror(errno));
goto err;
}
if (strcmp(pw->pw_name, username) != 0 && strcmp(pw->pw_name, "root") != 0) {
if (strcmp(username, "root") != 0) {
debug_dbg(cfg,
"The owner of the authentication file is neither %s nor root",
username);
} else {
debug_dbg(cfg, "The owner of the authentication file is not root");
}
goto err;
}
opwfile = fdopen(fd, "r");
if (opwfile == NULL) {
debug_dbg(cfg, "fdopen: %s", strerror(errno));
goto err;
} else {
fd = -1; /* fd belongs to opwfile */
}
if (cfg->sshformat == 0) {
if (parse_native_format(cfg, username, opwfile, devices, n_devs) != 1) {
goto err;
}
} else {
if (parse_ssh_format(cfg, opwfile, opwfile_size, devices, n_devs) != 1) {
goto err;
}
}
debug_dbg(cfg, "Found %d device(s) for user %s", *n_devs, username);
r = PAM_SUCCESS;
err:
if (r != PAM_SUCCESS) {
for (i = 0; i < *n_devs; i++) {
reset_device(&devices[i]);
}
*n_devs = 0;
} else if (*n_devs == 0) {
r = cfg->nouserok ? PAM_IGNORE : PAM_USER_UNKNOWN;
}
if (opwfile)
fclose(opwfile);
if (fd != -1)
close(fd);
return r;
}
void free_devices(device_t *devices, const unsigned n_devs) {
unsigned i;
if (!devices)
return;
for (i = 0; i < n_devs; i++) {
reset_device(&devices[i]);
}
free(devices);
devices = NULL;
}
static int get_authenticators(const cfg_t *cfg, const fido_dev_info_t *devlist,
size_t devlist_len, fido_assert_t *assert,
const int rk, fido_dev_t **authlist) {
const fido_dev_info_t *di = NULL;
fido_dev_t *dev = NULL;
int r;
size_t i;
size_t j;
debug_dbg(cfg, "Working with %zu authenticator(s)", devlist_len);
for (i = 0, j = 0; i < devlist_len; i++) {
debug_dbg(cfg, "Checking whether key exists in authenticator %zu", i);
di = fido_dev_info_ptr(devlist, i);
if (!di) {
debug_dbg(cfg, "Unable to get device pointer");
continue;
}
debug_dbg(cfg, "Authenticator path: %s", fido_dev_info_path(di));
dev = fido_dev_new();
if (!dev) {
debug_dbg(cfg, "Unable to allocate device type");
continue;
}
r = fido_dev_open(dev, fido_dev_info_path(di));
if (r != FIDO_OK) {
debug_dbg(cfg, "Failed to open authenticator: %s (%d)", fido_strerr(r),
r);
fido_dev_free(&dev);
continue;
}
if (rk || cfg->nodetect) {
/* resident credential or nodetect: try all authenticators */
authlist[j++] = dev;
} else {
r = fido_dev_get_assert(dev, assert, NULL);
if ((!fido_dev_is_fido2(dev) && r == FIDO_ERR_USER_PRESENCE_REQUIRED) ||
(fido_dev_is_fido2(dev) && r == FIDO_OK)) {
authlist[j++] = dev;
debug_dbg(cfg, "Found key in authenticator %zu", i);
return (1);
}
debug_dbg(cfg, "Key not found in authenticator %zu", i);
fido_dev_close(dev);
fido_dev_free(&dev);
}
}
if (j != 0)
return (1);
else {
debug_dbg(cfg, "Key not found");
return (0);
}
}
static void init_opts(struct opts *opts) {
opts->up = FIDO_OPT_FALSE;
opts->uv = FIDO_OPT_OMIT;
opts->pin = FIDO_OPT_FALSE;
}
static void parse_opts(const cfg_t *cfg, const char *attr, struct opts *opts) {
if (cfg->userpresence == 1 || strstr(attr, "+presence")) {
opts->up = FIDO_OPT_TRUE;
} else if (cfg->userpresence == 0) {
opts->up = FIDO_OPT_FALSE;
} else {
opts->up = FIDO_OPT_OMIT;
}
if (cfg->userverification == 1 || strstr(attr, "+verification")) {
opts->uv = FIDO_OPT_TRUE;
} else if (cfg->userverification == 0)
opts->uv = FIDO_OPT_FALSE;
else {
opts->uv = FIDO_OPT_OMIT;
}
if (cfg->pinverification == 1 || strstr(attr, "+pin")) {
opts->pin = FIDO_OPT_TRUE;
} else if (cfg->pinverification == 0) {
opts->pin = FIDO_OPT_FALSE;
} else {
opts->pin = FIDO_OPT_OMIT;
}
}
static int get_device_opts(fido_dev_t *dev, int *pin, int *uv) {
fido_cbor_info_t *info = NULL;
char *const *ptr;
const bool *val;
size_t len;
*pin = *uv = -1; /* unsupported */
if (fido_dev_is_fido2(dev)) {
if ((info = fido_cbor_info_new()) == NULL ||
fido_dev_get_cbor_info(dev, info) != FIDO_OK) {
fido_cbor_info_free(&info);
return 0;
}
ptr = fido_cbor_info_options_name_ptr(info);
val = fido_cbor_info_options_value_ptr(info);
len = fido_cbor_info_options_len(info);
for (size_t i = 0; i < len; i++) {
if (strcmp(ptr[i], "clientPin") == 0) {
*pin = val[i];
} else if (strcmp(ptr[i], "uv") == 0) {
*uv = val[i];
}
}
}
fido_cbor_info_free(&info);
return 1;
}
static int match_device_opts(fido_dev_t *dev, struct opts *opts) {
int pin, uv;
/* FIXME: fido_dev_{supports,has}_{pin,uv} (1.7.0) */
if (!get_device_opts(dev, &pin, &uv)) {
return -1;
}
if (opts->uv == FIDO_OPT_FALSE && uv < 0) {
opts->uv = FIDO_OPT_OMIT;
}
if ((opts->pin == FIDO_OPT_TRUE && pin != 1) ||
(opts->uv == FIDO_OPT_TRUE && uv != 1)) {
return 0;
}
return 1;
}
static int set_opts(const cfg_t *cfg, const struct opts *opts,
fido_assert_t *assert) {
if (fido_assert_set_up(assert, opts->up) != FIDO_OK) {
debug_dbg(cfg, "Failed to set UP");
return 0;
}
if (fido_assert_set_uv(assert, opts->uv) != FIDO_OK) {
debug_dbg(cfg, "Failed to set UV");
return 0;
}
return 1;
}
static int set_cdh(const cfg_t *cfg, fido_assert_t *assert) {
unsigned char cdh[32];
int r;
if (!random_bytes(cdh, sizeof(cdh))) {
debug_dbg(cfg, "Failed to generate challenge");
return 0;
}
r = fido_assert_set_clientdata_hash(assert, cdh, sizeof(cdh));
if (r != FIDO_OK) {
debug_dbg(cfg, "Unable to set challenge: %s (%d)", fido_strerr(r), r);
return 0;
}
return 1;
}
static fido_assert_t *prepare_assert(const cfg_t *cfg, const device_t *device,
const struct opts *opts) {
fido_assert_t *assert = NULL;
unsigned char *buf = NULL;
size_t buf_len;
int ok = 0;
int r;
if ((assert = fido_assert_new()) == NULL) {
debug_dbg(cfg, "Unable to allocate assertion");
goto err;
}
if (device->old_format)
r = fido_assert_set_rp(assert, cfg->appid);
else
r = fido_assert_set_rp(assert, cfg->origin);
if (r != FIDO_OK) {
debug_dbg(cfg, "Unable to set origin: %s (%d)", fido_strerr(r), r);
goto err;
}
if (is_resident(device->keyHandle)) {
debug_dbg(cfg, "Credential is resident");
} else {
debug_dbg(cfg, "Key handle: %s", device->keyHandle);
if (!b64_decode(device->keyHandle, (void **) &buf, &buf_len)) {
debug_dbg(cfg, "Failed to decode key handle");
goto err;
}
r = fido_assert_allow_cred(assert, buf, buf_len);
if (r != FIDO_OK) {
debug_dbg(cfg, "Unable to set keyHandle: %s (%d)", fido_strerr(r), r);
goto err;
}
}
if (!set_opts(cfg, opts, assert)) {
debug_dbg(cfg, "Failed to set assert options");
goto err;
}
if (!set_cdh(cfg, assert)) {
debug_dbg(cfg, "Failed to set client data hash");
goto err;
}
ok = 1;
err:
if (!ok)
fido_assert_free(&assert);
free(buf);
return assert;
}
static void reset_pk(struct pk *pk) {
if (pk->type == COSE_ES256) {
es256_pk_free((es256_pk_t **) &pk->ptr);
} else if (pk->type == COSE_RS256) {
rs256_pk_free((rs256_pk_t **) &pk->ptr);
} else if (pk->type == COSE_EDDSA) {
eddsa_pk_free((eddsa_pk_t **) &pk->ptr);
}
memset(pk, 0, sizeof(*pk));
}
int cose_type(const char *str, int *type) {
if (strcasecmp(str, "es256") == 0) {
*type = COSE_ES256;
} else if (strcasecmp(str, "rs256") == 0) {
*type = COSE_RS256;
} else if (strcasecmp(str, "eddsa") == 0) {
*type = COSE_EDDSA;
} else {
*type = 0;
return 0;
}
return 1;
}
const char *cose_string(int type) {
switch (type) {
case COSE_ES256:
return "es256";
case COSE_RS256:
return "rs256";
case COSE_EDDSA:
return "eddsa";
default:
return "unknown";
}
}
static int parse_pk(const cfg_t *cfg, int old, const char *type, const char *pk,
struct pk *out) {
unsigned char *buf = NULL;
size_t buf_len;
int ok = 0;
int r;
reset_pk(out);
if (old) {
if (!hex_decode(pk, &buf, &buf_len)) {
debug_dbg(cfg, "Failed to decode public key");
goto err;
}
} else {
if (!b64_decode(pk, (void **) &buf, &buf_len)) {
debug_dbg(cfg, "Failed to decode public key");
goto err;
}
}
if (!cose_type(type, &out->type)) {
debug_dbg(cfg, "Unknown COSE type '%s'", type);
goto err;
}
// For backwards compatibility, failure to pack the public key is not
// returned as an error. Instead, it is handled by fido_verify_assert().
if (out->type == COSE_ES256) {
if ((out->ptr = es256_pk_new()) == NULL) {
debug_dbg(cfg, "Failed to allocate ES256 public key");
goto err;
}
if (old) {
r = translate_old_format_pubkey(out->ptr, buf, buf_len);
} else {
r = es256_pk_from_ptr(out->ptr, buf, buf_len);
}
if (r != FIDO_OK) {
debug_dbg(cfg, "Failed to convert ES256 public key");
}
} else if (out->type == COSE_RS256) {
if ((out->ptr = rs256_pk_new()) == NULL) {
debug_dbg(cfg, "Failed to allocate RS256 public key");
goto err;
}
r = rs256_pk_from_ptr(out->ptr, buf, buf_len);
if (r != FIDO_OK) {
debug_dbg(cfg, "Failed to convert RS256 public key");
}
} else if (out->type == COSE_EDDSA) {
if ((out->ptr = eddsa_pk_new()) == NULL) {
debug_dbg(cfg, "Failed to allocate EDDSA public key");
goto err;
}
r = eddsa_pk_from_ptr(out->ptr, buf, buf_len);
if (r != FIDO_OK) {
debug_dbg(cfg, "Failed to convert EDDSA public key");
}
} else {
debug_dbg(cfg, "COSE type '%s' not handled", type);
goto err;
}
ok = 1;
err:
free(buf);
return ok;
}
int do_authentication(const cfg_t *cfg, const device_t *devices,
const unsigned n_devs, pam_handle_t *pamh) {
fido_assert_t *assert = NULL;
fido_dev_info_t *devlist = NULL;
fido_dev_t **authlist = NULL;
int cued = 0;
int r;
int retval = PAM_AUTH_ERR;
size_t ndevs = 0;
size_t ndevs_prev = 0;
unsigned i = 0;
struct opts opts;
struct pk pk;
char *pin = NULL;
init_opts(&opts);
#ifndef WITH_FUZZING
fido_init(cfg->debug ? FIDO_DEBUG : 0);
#else
fido_init(0);
#endif
memset(&pk, 0, sizeof(pk));
devlist = fido_dev_info_new(DEVLIST_LEN);
if (!devlist) {
debug_dbg(cfg, "Unable to allocate devlist");
goto out;
}
r = fido_dev_info_manifest(devlist, DEVLIST_LEN, &ndevs);
if (r != FIDO_OK) {
debug_dbg(cfg, "Unable to discover device(s), %s (%d)", fido_strerr(r), r);
goto out;
}
ndevs_prev = ndevs;
debug_dbg(cfg, "Device max index is %zu", ndevs);
authlist = calloc(DEVLIST_LEN + 1, sizeof(fido_dev_t *));
if (!authlist) {
debug_dbg(cfg, "Unable to allocate authenticator list");
goto out;
}
if (cfg->nodetect)
debug_dbg(cfg, "nodetect option specified, suitable key detection will be "
"skipped");
i = 0;
while (i < n_devs) {
debug_dbg(cfg, "Attempting authentication with device number %d", i + 1);
init_opts(&opts); /* used during authenticator discovery */
assert = prepare_assert(cfg, &devices[i], &opts);
if (assert == NULL) {
debug_dbg(cfg, "Failed to prepare assert");
goto out;
}
if (!parse_pk(cfg, devices[i].old_format, devices[i].coseType,
devices[i].publicKey, &pk)) {
debug_dbg(cfg, "Failed to parse public key");
goto out;
}
if (get_authenticators(cfg, devlist, ndevs, assert,
is_resident(devices[i].keyHandle), authlist)) {
for (size_t j = 0; authlist[j] != NULL; j++) {
/* options used during authentication */
parse_opts(cfg, devices[i].attributes, &opts);
r = match_device_opts(authlist[j], &opts);
if (r != 1) {
debug_dbg(cfg, "%s, skipping authenticator",
r < 0 ? "Failed to query supported options"
: "Unsupported options");
continue;
}
if (!set_opts(cfg, &opts, assert)) {
debug_dbg(cfg, "Failed to set assert options");
goto out;
}
if (!set_cdh(cfg, assert)) {
debug_dbg(cfg, "Failed to reset client data hash");
goto out;
}
if (opts.pin == FIDO_OPT_TRUE) {
pin = converse(pamh, PAM_PROMPT_ECHO_OFF, "Please enter the PIN: ");
if (pin == NULL) {
debug_dbg(cfg, "converse() returned NULL");
goto out;
}
}
if (opts.up == FIDO_OPT_TRUE || opts.uv == FIDO_OPT_TRUE) {
if (cfg->manual == 0 && cfg->cue && !cued) {
cued = 1;
converse(pamh, PAM_TEXT_INFO,
cfg->cue_prompt != NULL ? cfg->cue_prompt : DEFAULT_CUE);
}
}
r = fido_dev_get_assert(authlist[j], assert, pin);
if (pin) {
explicit_bzero(pin, strlen(pin));
free(pin);
pin = NULL;
}
if (r == FIDO_OK) {
if (opts.pin == FIDO_OPT_TRUE || opts.uv == FIDO_OPT_TRUE) {
r = fido_assert_set_uv(assert, FIDO_OPT_TRUE);
if (r != FIDO_OK) {
debug_dbg(cfg, "Failed to set UV");
goto out;
}
}
r = fido_assert_verify(assert, 0, pk.type, pk.ptr);
if (r == FIDO_OK) {
retval = PAM_SUCCESS;
goto out;
}
}
}
} else {
debug_dbg(cfg, "Device for this keyhandle is not present");
}
i++;
fido_dev_info_free(&devlist, ndevs);
devlist = fido_dev_info_new(DEVLIST_LEN);
if (!devlist) {
debug_dbg(cfg, "Unable to allocate devlist");
goto out;
}
r = fido_dev_info_manifest(devlist, DEVLIST_LEN, &ndevs);
if (r != FIDO_OK) {
debug_dbg(cfg, "Unable to discover device(s), %s (%d)", fido_strerr(r),
r);
goto out;
}
if (ndevs > ndevs_prev) {
debug_dbg(cfg,
"Devices max_index has changed: %zu (was %zu). Starting over",
ndevs, ndevs_prev);
ndevs_prev = ndevs;
i = 0;
}
for (size_t j = 0; authlist[j] != NULL; j++) {
fido_dev_close(authlist[j]);
fido_dev_free(&authlist[j]);
}
fido_assert_free(&assert);
}
out:
reset_pk(&pk);
fido_assert_free(&assert);
fido_dev_info_free(&devlist, ndevs);
if (authlist) {
for (size_t j = 0; authlist[j] != NULL; j++) {
fido_dev_close(authlist[j]);
fido_dev_free(&authlist[j]);
}
free(authlist);
}
return retval;
}
#define MAX_PROMPT_LEN (1024)
static int manual_get_assert(const cfg_t *cfg, const char *prompt,
pam_handle_t *pamh, fido_assert_t *assert) {
char *b64_cdh = NULL;
char *b64_rpid = NULL;
char *b64_authdata = NULL;
char *b64_sig = NULL;
unsigned char *authdata = NULL;
unsigned char *sig = NULL;
size_t authdata_len;
size_t sig_len;
int r;
int ok = 0;
b64_cdh = converse(pamh, PAM_PROMPT_ECHO_ON, prompt);
b64_rpid = converse(pamh, PAM_PROMPT_ECHO_ON, prompt);
b64_authdata = converse(pamh, PAM_PROMPT_ECHO_ON, prompt);
b64_sig = converse(pamh, PAM_PROMPT_ECHO_ON, prompt);
if (!b64_decode(b64_authdata, (void **) &authdata, &authdata_len)) {
debug_dbg(cfg, "Failed to decode authenticator data");
goto err;
}
if (!b64_decode(b64_sig, (void **) &sig, &sig_len)) {
debug_dbg(cfg, "Failed to decode signature");
goto err;
}
r = fido_assert_set_count(assert, 1);
if (r != FIDO_OK) {
debug_dbg(cfg, "Failed to set signature count of assertion");
goto err;
}
r = fido_assert_set_authdata(assert, 0, authdata, authdata_len);
if (r != FIDO_OK) {
debug_dbg(cfg, "Failed to set authdata of assertion");
goto err;
}
r = fido_assert_set_sig(assert, 0, sig, sig_len);
if (r != FIDO_OK) {
debug_dbg(cfg, "Failed to set signature of assertion");
goto err;
}
ok = 1;
err:
free(b64_cdh);
free(b64_rpid);
free(b64_authdata);
free(b64_sig);
free(authdata);
free(sig);
return ok;
}
int do_manual_authentication(const cfg_t *cfg, const device_t *devices,
const unsigned n_devs, pam_handle_t *pamh) {
fido_assert_t **assert = NULL;
struct pk *pk = NULL;
char *b64_challenge = NULL;
char prompt[MAX_PROMPT_LEN];
char buf[MAX_PROMPT_LEN];
int retval = PAM_AUTH_ERR;
int n;
int r;
unsigned i = 0;
struct opts opts;
init_opts(&opts);
assert = calloc(n_devs, sizeof(*assert));
if (assert == NULL)
goto out;
pk = calloc(n_devs, sizeof(*pk));
if (pk == NULL)
goto out;
#ifndef WITH_FUZZING
fido_init(cfg->debug ? FIDO_DEBUG : 0);
#else
fido_init(0);
#endif
for (i = 0; i < n_devs; ++i) {
/* options used during authentication */
parse_opts(cfg, devices[i].attributes, &opts);
assert[i] = prepare_assert(cfg, &devices[i], &opts);
if (assert[i] == NULL) {
debug_dbg(cfg, "Failed to prepare assert");
goto out;
}
debug_dbg(cfg, "Attempting authentication with device number %d", i + 1);
if (!parse_pk(cfg, devices[i].old_format, devices[i].coseType,
devices[i].publicKey, &pk[i])) {
debug_dbg(cfg, "Unable to parse public key %u", i);
goto out;
}
if (!b64_encode(fido_assert_clientdata_hash_ptr(assert[i]),
fido_assert_clientdata_hash_len(assert[i]),
&b64_challenge)) {
debug_dbg(cfg, "Failed to encode challenge");
goto out;
}
debug_dbg(cfg, "Challenge: %s", b64_challenge);
n = snprintf(prompt, sizeof(prompt), "Challenge #%d:", i + 1);
if (n <= 0 || (size_t) n >= sizeof(prompt)) {
debug_dbg(cfg, "Failed to print challenge prompt");
goto out;
}
converse(pamh, PAM_TEXT_INFO, prompt);
n = snprintf(buf, sizeof(buf), "%s\n%s\n%s", b64_challenge, cfg->origin,
devices[i].keyHandle);
if (n <= 0 || (size_t) n >= sizeof(buf)) {
debug_dbg(cfg, "Failed to print fido2-assert input string");
goto out;
}
converse(pamh, PAM_TEXT_INFO, buf);
free(b64_challenge);
b64_challenge = NULL;
}
converse(pamh, PAM_TEXT_INFO,
"Please pass the challenge(s) above to fido2-assert, and "
"paste the results in the prompt below.");
for (i = 0; i < n_devs; ++i) {
n = snprintf(prompt, sizeof(prompt), "Response #%d: ", i + 1);
if (n <= 0 || (size_t) n >= sizeof(prompt)) {
debug_dbg(cfg, "Failed to print response prompt");
goto out;
}
if (!manual_get_assert(cfg, prompt, pamh, assert[i])) {
debug_dbg(cfg, "Failed to get assert %u", i);
goto out;
}
r = fido_assert_verify(assert[i], 0, pk[i].type, pk[i].ptr);
if (r == FIDO_OK) {
retval = PAM_SUCCESS;
break;
}
}
out:
for (i = 0; i < n_devs; i++) {
fido_assert_free(&assert[i]);
reset_pk(&pk[i]);
}
free(assert);
free(pk);
free(b64_challenge);
return retval;
}
static int _converse(pam_handle_t *pamh, int nargs,
const struct pam_message **message,
struct pam_response **response) {
struct pam_conv *conv;
int retval;
retval = pam_get_item(pamh, PAM_CONV, (void *) &conv);
if (retval != PAM_SUCCESS) {
return retval;
}
return conv->conv(nargs, message, response, conv->appdata_ptr);
}
char *converse(pam_handle_t *pamh, int echocode, const char *prompt) {
const struct pam_message msg = {.msg_style = echocode,
.msg = (char *) (uintptr_t) prompt};
const struct pam_message *msgs = &msg;
struct pam_response *resp = NULL;
int retval = _converse(pamh, 1, &msgs, &resp);
char *ret = NULL;
if (retval != PAM_SUCCESS || resp == NULL || resp->resp == NULL ||
*resp->resp == '\000') {
if (retval == PAM_SUCCESS && resp && resp->resp) {
ret = resp->resp;
}
} else {
ret = resp->resp;
}
// Deallocate temporary storage.
if (resp) {
if (!ret) {
free(resp->resp);
}
free(resp);
}
return ret;
}
#ifndef RANDOM_DEV
#define RANDOM_DEV "/dev/urandom"
#endif
int random_bytes(void *buf, size_t cnt) {
int fd;
ssize_t n;
fd = open(RANDOM_DEV, O_RDONLY);
if (fd < 0)
return (0);
n = read(fd, buf, cnt);
close(fd);
if (n < 0 || (size_t) n != cnt)
return (0);
return (1);
}