| /* |
| * duo.c |
| * |
| * Copyright (c) 2010 Duo Security |
| * All rights reserved, all wrongs reversed. |
| */ |
| |
| #include "config.h" |
| |
| #include <sys/types.h> |
| #include <sys/socket.h> |
| #include <sys/stat.h> |
| |
| #include <arpa/inet.h> |
| #include <netinet/in.h> |
| |
| #include <assert.h> |
| #include <ctype.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <netdb.h> |
| #include <stdarg.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <unistd.h> |
| |
| #include <openssl/bio.h> |
| #include <openssl/evp.h> |
| #include <openssl/hmac.h> |
| #include <openssl/sha.h> |
| #include <openssl/ssl.h> |
| |
| #include "util.h" |
| #include "bson.h" |
| #include "duo.h" |
| #include "https.h" |
| #include "ini.h" |
| #include "urlenc.h" |
| |
| #define DUO_LIB_VERSION "libduo/" PACKAGE_VERSION |
| #define DUO_API_VERSION "/rest/v1" |
| #define AUTOPUSH_MSG "Autopushing login request to phone..." |
| #define AUTOPHONE_MSG "Calling your phone..." |
| #define AUTODEFAULT_MSG "Using default second-factor authentication." |
| #define ENV_VAR_MSG "Reading $DUO_PASSCODE..." |
| |
| /* |
| * Finding the maximum length for the machine's hostname |
| * Idea and technique originated from https://github.com/openssh/openssh-portable |
| */ |
| #ifndef HOST_NAME_MAX |
| # include "netdb.h" /* for MAXHOSTNAMELEN */ |
| # if defined(_POSIX_HOST_NAME_MAX) |
| # define HOST_NAME_MAX _POSIX_HOST_NAME_MAX |
| # elif defined(MAXHOSTNAMELEN) |
| # define HOST_NAME_MAX MAXHOSTNAMELEN |
| # else |
| # define HOST_NAME_MAX 255 |
| # endif |
| #endif /* HOST_NAME_MAX */ |
| |
| /* For sizing buffers; sufficient to cover the longest possible DNS FQDN */ |
| #define DNS_MAXNAMELEN 256 |
| |
| struct duo_ctx { |
| https_t *https; /* HTTPS handle */ |
| char *host; /* host[:port] */ |
| char err[512]; /* error message */ |
| |
| char *argv[16]; /* request arguments */ |
| int argc; |
| |
| const char *body; /* response body */ |
| int body_len; |
| |
| int https_timeout; /* milliseconds */ |
| |
| char *ikey; |
| char *skey; |
| char *useragent; |
| |
| char *(*conv_prompt)(void *arg, const char *pr, char *buf, size_t sz); |
| void (*conv_status)(void *arg, const char *msg); |
| void *conv_arg; |
| }; |
| |
| static char * |
| __prompt_fn(void *arg, const char *prompt, char *buf, size_t bufsz) |
| { |
| printf("%s", prompt); |
| fflush(stdout); |
| return (fgets(buf, bufsz, stdin)); |
| } |
| |
| static void |
| __status_fn(void *arg, const char *msg) |
| { |
| printf("%s\n", msg); |
| } |
| |
| struct duo_ctx * |
| duo_open(const char *host, const char *ikey, const char *skey, |
| const char *progname, const char *cafile, int https_timeout, const char* http_proxy) |
| { |
| struct duo_ctx *ctx; |
| |
| if ((ctx = calloc(1, sizeof(*ctx))) == NULL || |
| (ctx->host = strdup(host)) == NULL || |
| (ctx->ikey = strdup(ikey)) == NULL || |
| (ctx->skey = strdup(skey)) == NULL) { |
| return (duo_close(ctx)); |
| } |
| if (asprintf(&ctx->useragent, "%s (%s) libduo/%s", |
| progname, CANONICAL_HOST, PACKAGE_VERSION) == -1) { |
| return (duo_close(ctx)); |
| } |
| if (https_init(cafile, http_proxy) != HTTPS_OK) { |
| ctx = duo_close(ctx); |
| } else { |
| ctx->conv_prompt = __prompt_fn; |
| ctx->conv_status = __status_fn; |
| ctx->https_timeout = https_timeout; |
| } |
| |
| return (ctx); |
| } |
| |
| int |
| duo_parse_config(const char *filename, |
| int (*callback)(void *arg, const char *section, |
| const char *name, const char *val), void *arg) |
| { |
| FILE *fp; |
| struct stat st; |
| int fd, ret; |
| |
| if ((fd = open(filename, O_RDONLY)) < 0) { |
| return (-1); |
| } |
| if (fstat(fd, &st) < 0 || (fp = fdopen(fd, "r")) == NULL) { |
| close(fd); |
| return (-1); |
| } |
| if ((st.st_mode & (S_IRGRP|S_IROTH)) != 0) { |
| fclose(fp); |
| return (-2); |
| } |
| ret = ini_parse(fp, callback, arg); |
| fclose(fp); |
| return (ret); |
| } |
| |
| static duo_code_t |
| duo_reset(struct duo_ctx *ctx) |
| { |
| int i; |
| |
| for (i = 0; i < ctx->argc; i++) { |
| free(ctx->argv[i]); |
| ctx->argv[i] = NULL; |
| } |
| ctx->argc = 0; |
| *ctx->err = '\0'; |
| |
| return (DUO_OK); |
| } |
| |
| struct duo_ctx * |
| duo_close(struct duo_ctx *ctx) |
| { |
| if (ctx != NULL) { |
| if (ctx->https != NULL) { |
| https_close(&ctx->https); |
| } |
| duo_reset(ctx); |
| free(ctx->host); |
| |
| if (ctx->ikey != NULL) { |
| duo_zero_free(ctx->ikey, strlen(ctx->ikey)); |
| ctx->ikey = NULL; |
| } |
| if (ctx->skey != NULL) { |
| duo_zero_free(ctx->skey, strlen(ctx->skey)); |
| ctx->skey = NULL; |
| } |
| if (ctx->useragent != NULL) { |
| duo_zero_free(ctx->useragent, strlen(ctx->useragent)); |
| ctx->useragent = NULL; |
| } |
| |
| free(ctx); |
| } |
| return (NULL); |
| } |
| |
| void |
| duo_set_conv_funcs(struct duo_ctx *ctx, |
| char *(*prompt_fn)(void *arg, const char *prompt, char *buf, size_t bufsz), |
| void (*status_fn)(void *arg, const char *msg), |
| void *arg) |
| { |
| ctx->conv_prompt = prompt_fn; |
| ctx->conv_status = status_fn; |
| ctx->conv_arg = arg; |
| } |
| |
| void |
| duo_reset_conv_funcs(struct duo_ctx *ctx) |
| { |
| ctx->conv_prompt = __prompt_fn; |
| ctx->conv_status = __status_fn; |
| } |
| |
| static duo_code_t |
| duo_add_param(struct duo_ctx *ctx, const char *name, const char *value) |
| { |
| duo_code_t ret; |
| char *k, *v, *p; |
| |
| if (name == NULL || value == NULL || strlen(name) == 0 || strlen(value) == 0) { |
| return (DUO_CLIENT_ERROR); |
| } |
| ret = DUO_LIB_ERROR; |
| |
| k = urlenc_encode(name); |
| v = urlenc_encode(value); |
| |
| if (k && v && ctx->argc + 1 < (sizeof(ctx->argv) / sizeof(ctx->argv[0])) |
| && (asprintf(&p, "%s=%s", k, v) > 2)) { |
| ctx->argv[ctx->argc++] = p; |
| ret = DUO_OK; |
| } |
| |
| free(k); |
| free(v); |
| |
| return (ret); |
| } |
| |
| static duo_code_t |
| duo_add_optional_param(struct duo_ctx *ctx, const char *name, const char *value) |
| { |
| /* Wrapper around duo_add_param for optional arguments. |
| If a parameter's value doesn't exist we don't add the param. |
| */ |
| if (value == NULL || strlen(value) == 0) { |
| return DUO_OK; |
| } |
| else { |
| return duo_add_param(ctx, name, value); |
| } |
| } |
| |
| static void |
| _duo_seterr(struct duo_ctx *ctx, const char *fmt, ...) |
| { |
| va_list ap; |
| |
| va_start(ap, fmt); |
| vsnprintf(ctx->err, sizeof(ctx->err), fmt, ap); |
| va_end(ap); |
| } |
| |
| |
| static void |
| _duo_get_hostname(char *dns_fqdn, size_t dns_fqdn_size) |
| { |
| struct addrinfo hints, *info, *p; |
| char hostname[HOST_NAME_MAX + 1]; |
| |
| /* gethostname may not insert a null terminator when it needs to truncate the hostname. |
| * See gethostname's man page under "Description" for more info. |
| */ |
| hostname[HOST_NAME_MAX] = '\0'; |
| gethostname(hostname, HOST_NAME_MAX); |
| memset(&hints, 0, sizeof(hints)); |
| |
| hints.ai_family = AF_UNSPEC; |
| hints.ai_socktype = SOCK_STREAM; |
| hints.ai_flags = AI_CANONNAME; |
| strlcpy(dns_fqdn, hostname, dns_fqdn_size); |
| |
| if (getaddrinfo(hostname, NULL, &hints, &info) == 0) { |
| if(info->ai_canonname != NULL && strlen(info->ai_canonname) > 0) { |
| strlcpy(dns_fqdn, info->ai_canonname, dns_fqdn_size); |
| } |
| freeaddrinfo(info); |
| } |
| } |
| |
| int |
| _duo_add_hostname_param(struct duo_ctx *ctx) |
| { |
| char dns_fqdn[DNS_MAXNAMELEN]; |
| _duo_get_hostname(dns_fqdn, sizeof(dns_fqdn)); |
| |
| return duo_add_optional_param(ctx, "hostname", dns_fqdn); |
| } |
| |
| #define _BSON_FIND(ctx, it, obj, name, type) do { \ |
| if (bson_find(it, obj, name) != type) { \ |
| _duo_seterr(ctx, "BSON missing valid '%s'", name); \ |
| return (DUO_SERVER_ERROR); \ |
| } \ |
| } while (0) |
| |
| static duo_code_t |
| _duo_bson_response(struct duo_ctx *ctx, bson *resp) |
| { |
| bson obj; |
| bson_iterator it; |
| duo_code_t ret; |
| const char *p; |
| int code; |
| |
| bson_init(&obj, (char *)ctx->body, 0); |
| |
| ret = DUO_SERVER_ERROR; |
| |
| if (ctx->body_len <= 0 || bson_size(&obj) > ctx->body_len) { |
| _duo_seterr(ctx, "invalid BSON response"); |
| return (ret); |
| } |
| _BSON_FIND(ctx, &it, &obj, "stat", bson_string); |
| p = bson_iterator_string(&it); |
| |
| if (strcasecmp(p, "OK") == 0) { |
| _BSON_FIND(ctx, &it, &obj, "response", bson_object); |
| if (resp) { |
| bson_iterator_subobject(&it, resp); |
| } |
| ret = DUO_OK; |
| } else if (strcasecmp(p, "FAIL") == 0) { |
| _BSON_FIND(ctx, &it, &obj, "code", bson_int); |
| code = bson_iterator_int(&it); |
| _BSON_FIND(ctx, &it, &obj, "message", bson_string); |
| _duo_seterr(ctx, "%d: %s", code, bson_iterator_string(&it)); |
| ret = DUO_FAIL; |
| } |
| return (ret); |
| } |
| |
| static duo_code_t |
| duo_call(struct duo_ctx *ctx, const char *method, const char *uri, int msecs) |
| { |
| int i, code, err, ret; |
| |
| code = 0; |
| ctx->body = NULL; |
| ctx->body_len = 0; |
| |
| for (i = 0; i < 3; i++) { |
| if (ctx->https == NULL && |
| (err = https_open(&ctx->https, ctx->host, ctx->useragent)) != HTTPS_OK) { |
| if (err == HTTPS_ERR_SERVER) { |
| sleep(1 << i); |
| continue; |
| } |
| break; |
| } |
| if ((err = https_send(ctx->https, method, uri, |
| ctx->argc, ctx->argv, ctx->ikey, ctx->skey, ctx->useragent)) == HTTPS_OK && |
| (err = https_recv(ctx->https, &code, |
| &ctx->body, &ctx->body_len, msecs)) == HTTPS_OK) { |
| break; |
| } |
| https_close(&ctx->https); |
| } |
| duo_reset(ctx); |
| |
| if (code == 0) { |
| ret = DUO_CONN_ERROR; |
| _duo_seterr(ctx, "Couldn't connect to %s: %s\n", |
| ctx->host, https_geterr()); |
| } else if (code / 100 == 2) { |
| /* 2xx indicates DUO_OK */ |
| ret = DUO_OK; |
| } else if (code == 401) { |
| /* 401 indicates an invalid ikey or skey */ |
| ret = DUO_CLIENT_ERROR; |
| _duo_seterr(ctx, "Invalid ikey or skey"); |
| } else if (code / 100 == 5) { |
| /* 5xx indicates an internal server error */ |
| ret = DUO_SERVER_ERROR; |
| _duo_seterr(ctx, "HTTP %d", code); |
| } else { |
| /* abort on any other HTTP codes */ |
| ret = DUO_ABORT; |
| _duo_seterr(ctx, "HTTP %d", code); |
| } |
| return (ret); |
| } |
| |
| const char * |
| duo_geterr(struct duo_ctx *ctx) |
| { |
| return (ctx->err[0] ? ctx->err : NULL); |
| } |
| |
| duo_code_t |
| _duo_preauth(struct duo_ctx *ctx, bson *obj, const char *username, |
| const char *client_ip) |
| { |
| bson_iterator it; |
| duo_code_t ret; |
| const char *p; |
| |
| /* Check preauth result */ |
| if (duo_add_param(ctx, "user", username) != DUO_OK) { |
| return (DUO_LIB_ERROR); |
| } |
| |
| if (duo_add_optional_param(ctx, "ipaddr", client_ip) != DUO_OK) { |
| return (DUO_LIB_ERROR); |
| } |
| |
| if(_duo_add_hostname_param(ctx) != DUO_OK) { |
| return (DUO_LIB_ERROR); |
| } |
| |
| if ((ret = duo_call(ctx, "POST", DUO_API_VERSION "/preauth.bson", ctx->https_timeout)) != DUO_OK || |
| (ret = _duo_bson_response(ctx, obj)) != DUO_OK) { |
| return (ret); |
| } |
| _BSON_FIND(ctx, &it, obj, "result", bson_string); |
| p = bson_iterator_string(&it); |
| |
| if (strcasecmp(p, "auth") != 0) { |
| _BSON_FIND(ctx, &it, obj, "status", bson_string); |
| if (strcasecmp(p, "allow") == 0) { |
| _duo_seterr(ctx, "%s", bson_iterator_string(&it)); |
| ret = DUO_OK; |
| } else if (strcasecmp(p, "deny") == 0) { |
| _duo_seterr(ctx, "%s", bson_iterator_string(&it)); |
| if (ctx->conv_status != NULL) { |
| ctx->conv_status(ctx->conv_arg, |
| bson_iterator_string(&it)); |
| } |
| ret = DUO_ABORT; |
| } else if (strcasecmp(p, "enroll") == 0) { |
| if (ctx->conv_status != NULL) { |
| ctx->conv_status(ctx->conv_arg, |
| bson_iterator_string(&it)); |
| } |
| _duo_seterr(ctx, "User enrollment required"); |
| ret = DUO_ABORT; |
| } else { |
| _duo_seterr(ctx, "BSON invalid 'result': %s", p); |
| ret = DUO_SERVER_ERROR; |
| } |
| return (ret); |
| } |
| return (DUO_CONTINUE); |
| } |
| |
| duo_code_t |
| _duo_prompt(struct duo_ctx *ctx, bson *obj, int flags, char *buf, |
| size_t sz, const char **p) |
| { |
| bson_iterator it; |
| char *pos, *passcode; |
| |
| passcode = getenv(DUO_ENV_VAR_NAME); |
| |
| if ((flags & DUO_FLAG_ENV) && (passcode != NULL)) { |
| *p = passcode; |
| if (ctx->conv_status != NULL) { |
| ctx->conv_status(ctx->conv_arg, ENV_VAR_MSG); |
| } |
| } else if ((flags & DUO_FLAG_AUTO) != 0) { |
| /* Find default OOB factor for automatic login */ |
| _BSON_FIND(ctx, &it, obj, "factors", bson_object); |
| bson_iterator_subobject(&it, obj); |
| |
| if (bson_find(&it, obj, "default") != bson_string) { |
| _duo_seterr(ctx, "No default factor found for automatic login"); |
| return (DUO_ABORT); |
| } |
| *p = bson_iterator_string(&it); |
| if (ctx->conv_status) { |
| if ((pos = strstr(*p, "push"))) { |
| ctx->conv_status(ctx->conv_arg, AUTOPUSH_MSG); |
| } else if ((pos = strstr(*p, "phone"))) { |
| ctx->conv_status(ctx->conv_arg, AUTOPHONE_MSG); |
| } else { |
| ctx->conv_status(ctx->conv_arg, AUTODEFAULT_MSG); |
| } |
| } |
| } else { |
| /* Prompt user for factor choice / token */ |
| if (ctx->conv_prompt == NULL) { |
| _duo_seterr(ctx, "No prompt function set"); |
| return (DUO_CLIENT_ERROR); |
| } |
| _BSON_FIND(ctx, &it, obj, "prompt", bson_string); |
| *p = bson_iterator_string(&it); |
| |
| if (ctx->conv_prompt(ctx->conv_arg, *p, buf, sz) == NULL) { |
| _duo_seterr(ctx, "Error gathering user response"); |
| return (DUO_ABORT); |
| } |
| strtok(buf, "\r\n"); |
| |
| _BSON_FIND(ctx, &it, obj, "factors", bson_object); |
| bson_iterator_subobject(&it, obj); |
| |
| if (bson_find(&it, obj, buf) == bson_string) { |
| *p = bson_iterator_string(&it); |
| } else { |
| *p = buf; |
| } |
| } |
| return (DUO_CONTINUE); |
| } |
| |
| duo_code_t |
| duo_login(struct duo_ctx *ctx, const char *username, |
| const char *client_ip, int flags, const char *command, const int failmode) |
| { |
| bson obj; |
| bson_iterator it; |
| duo_code_t ret; |
| char buf[256]; |
| char *pushinfo = NULL; |
| const char *p; |
| int i; |
| const char *local_ip; |
| |
| if (username == NULL) { |
| _duo_seterr(ctx, "need username to authenticate"); |
| return (DUO_CLIENT_ERROR); |
| } |
| |
| /* Check preauth status */ |
| if ((ret = _duo_preauth(ctx, &obj, username, client_ip)) != DUO_CONTINUE) { |
| if(ret == DUO_SERVER_ERROR || ret == DUO_CONN_ERROR || ret == DUO_CLIENT_ERROR) { |
| return (failmode == DUO_FAIL_SAFE) ? (DUO_FAIL_SAFE_ALLOW) : (DUO_FAIL_SECURE_DENY); |
| } |
| return (ret); |
| } |
| |
| /* Handle factor selection */ |
| if ((ret = _duo_prompt(ctx, &obj, flags, buf, sizeof(buf), &p)) != DUO_CONTINUE) { |
| return (ret); |
| } |
| |
| /* Add request parameters */ |
| if (duo_add_param(ctx, "user", username) != DUO_OK || |
| duo_add_param(ctx, "factor", "auto") != DUO_OK || |
| duo_add_param(ctx, "auto", p) != DUO_OK || |
| duo_add_param(ctx, "async", |
| (flags & DUO_FLAG_SYNC) ? "0" : "1") != DUO_OK) { |
| return (DUO_LIB_ERROR); |
| } |
| |
| /* Add client IP, if passed in */ |
| if (duo_add_optional_param(ctx, "ipaddr", client_ip) != DUO_OK) { |
| return (DUO_LIB_ERROR); |
| } |
| |
| if(_duo_add_hostname_param(ctx) != DUO_OK) { |
| return (DUO_LIB_ERROR); |
| } |
| |
| /* Add pushinfo parameters */ |
| local_ip = duo_local_ip(); |
| if (asprintf(&pushinfo, "Server+IP=%s&Command=%s", |
| local_ip, command ? urlenc_encode(command) : "") < 0 || |
| duo_add_param(ctx, "pushinfo", pushinfo) != DUO_OK) { |
| return (DUO_LIB_ERROR); |
| } |
| free(pushinfo); |
| |
| /* Try Duo authentication. Only use the configured timeout if |
| * the call is asynchronous, because async calls should return |
| * immediately. |
| */ |
| if ((ret = duo_call(ctx, "POST", DUO_API_VERSION "/auth.bson", |
| flags & DUO_FLAG_SYNC ? DUO_NO_TIMEOUT : ctx->https_timeout)) != DUO_OK || |
| (ret = _duo_bson_response(ctx, &obj)) != DUO_OK) { |
| return (ret); |
| } |
| |
| /* Handle sync status */ |
| if ((flags & DUO_FLAG_SYNC) != 0) { |
| _BSON_FIND(ctx, &it, &obj, "status", bson_string); |
| if (ctx->conv_status != NULL) { |
| ctx->conv_status(ctx->conv_arg, |
| bson_iterator_string(&it)); |
| } |
| _BSON_FIND(ctx, &it, &obj, "result", bson_string); |
| p = bson_iterator_string(&it); |
| |
| if (strcasecmp(p, "allow") == 0) { |
| ret = DUO_OK; |
| } else if (strcasecmp(p, "deny") == 0) { |
| ret = DUO_FAIL; |
| } else { |
| _duo_seterr(ctx, "BSON invalid 'result': %s", p); |
| ret = DUO_SERVER_ERROR; |
| } |
| return (ret); |
| } |
| /* Async status - long-poll on txid */ |
| _BSON_FIND(ctx, &it, &obj, "txid", bson_string); |
| p = bson_iterator_string(&it); |
| if (strlcpy(buf, p, sizeof(buf)) >= sizeof(buf)) { |
| return (DUO_LIB_ERROR); |
| } |
| /* XXX newline between prompt and async status lines */ |
| if (ctx->conv_status != NULL) { |
| ctx->conv_status(ctx->conv_arg, ""); |
| } |
| ret = DUO_SERVER_ERROR; |
| |
| for (i = 0; i < 20; i++) { |
| if ((ret = duo_add_param(ctx, "txid", buf)) != DUO_OK || |
| (ret = duo_call(ctx, "GET", |
| DUO_API_VERSION "/status.bson", DUO_NO_TIMEOUT)) != DUO_OK || |
| (ret = _duo_bson_response(ctx, &obj)) != DUO_OK) { |
| break; |
| } |
| if (bson_find(&it, &obj, "status") == bson_string) { |
| if (ctx->conv_status != NULL) { |
| ctx->conv_status(ctx->conv_arg, |
| bson_iterator_string(&it)); |
| } |
| } |
| if (bson_find(&it, &obj, "result") == bson_string) { |
| p = bson_iterator_string(&it); |
| |
| if (strcasecmp(p, "allow") == 0) { |
| ret = DUO_OK; |
| } else if (strcasecmp(p, "deny") == 0) { |
| ret = DUO_FAIL; |
| } else { |
| _duo_seterr(ctx, "BSON invalid 'result': %s", |
| p); |
| ret = DUO_SERVER_ERROR; |
| } |
| break; |
| } |
| } |
| return (ret); |
| } |