lib/duo.c (575 lines of code) (raw):

/* * 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 <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 "duo.h" #include "parson.h" #include "duo_private.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 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); // We need to add 1 here for the terminating \0 byte which strlen doesn't include if (ctx->ikey != NULL) { duo_zero_free(ctx->ikey, strlen(ctx->ikey) + 1); ctx->ikey = NULL; } if (ctx->skey != NULL) { duo_zero_free(ctx->skey, strlen(ctx->skey) + 1); ctx->skey = NULL; } if (ctx->useragent != NULL) { duo_zero_free(ctx->useragent, strlen(ctx->useragent) + 1); 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; } 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); } 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); } int _duo_add_failmode_param(struct duo_ctx *ctx, const int failmode) { const char *failmode_str = (failmode == DUO_FAIL_SECURE) ? ("closed") : ("open"); return duo_add_optional_param(ctx, "failmode", failmode_str); } #define _JSON_FIND_OBJECT(out_obj, in_obj, name, json_value) do { \ out_obj = json_object_get_object(in_obj, name); \ if (out_obj == NULL) { \ _duo_seterr(ctx, "JSON missing valid '%s'", name); \ _JSON_VALUE_FREE(json_value); \ return (DUO_SERVER_ERROR); \ } \ } while(0) #define _JSON_FIND_STRING(buf, json_obj, name, json_value) do { \ buf = json_object_get_string(json_obj, name); \ if (buf == NULL) { \ _duo_seterr(ctx, "JSON missing valid '%s'", name); \ _JSON_VALUE_FREE(json_value); \ return (DUO_SERVER_ERROR); \ } \ } while(0) # define _JSON_VALUE_FREE(value) do { \ json_value_free(value); \ value = NULL; \ } while(0) static duo_code_t _duo_json_response(struct duo_ctx *ctx) { JSON_Value *json; JSON_Object *json_obj; char *p; int code = DUO_SERVER_ERROR; json = json_parse_string(ctx->body); if(json == NULL) { _duo_seterr(ctx, "invalid JSON response"); return (DUO_SERVER_ERROR); } json_obj = json_value_get_object(json); _JSON_FIND_STRING(p, json_obj, "stat", json); if (strcasecmp(p, "OK") == 0) { code = DUO_OK; } if (strcasecmp(p, "FAIL") == 0) { char *message; code = json_object_get_number(json_obj, "code"); // json_object_get_number will return 0 if "code" not found if (code == 0) { _duo_seterr(ctx, "JSON missing valid 'code'"); _JSON_VALUE_FREE(json); return (DUO_SERVER_ERROR); } _JSON_FIND_STRING(message, json_obj, "message", json); _duo_seterr(ctx, "%d: %s", code, message); code = DUO_FAIL; } _JSON_VALUE_FREE(json); return code; } 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, const char *username, const char *client_ip, const int failmode) { duo_code_t ret; const char *p; JSON_Value *json; JSON_Object *json_obj; /* 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(_duo_add_failmode_param(ctx, failmode) != DUO_OK) { return (DUO_LIB_ERROR); } if ((ret = duo_call(ctx, "POST", DUO_API_VERSION "/preauth.json", ctx->https_timeout)) != DUO_OK || (ret = _duo_json_response(ctx)) != DUO_OK) { return (ret); } json = json_parse_string(ctx->body); json_obj = json_value_get_object(json); JSON_Object *response; _JSON_FIND_OBJECT(response, json_obj, "response", json); _JSON_FIND_STRING(p, response, "result", json); if (p == NULL) { _duo_seterr(ctx, "JSON invalid 'result': %s", p); ret = DUO_SERVER_ERROR; } else if (strcasecmp(p, "auth") != 0) { char *output; _JSON_FIND_STRING(output, response, "status", json); if (strcasecmp(p, "allow") == 0) { _duo_seterr(ctx, "%s", output); ret = DUO_OK; } else if (strcasecmp(p, "deny") == 0) { _duo_seterr(ctx, "%s", output); if (ctx->conv_status != NULL) { ctx->conv_status(ctx->conv_arg, output); } ret = DUO_ABORT; } else if (strcasecmp(p, "enroll") == 0) { if (ctx->conv_status != NULL) { ctx->conv_status(ctx->conv_arg, output); } _duo_seterr(ctx, "User enrollment required"); ret = DUO_ABORT; } else { _duo_seterr(ctx, "JSON invalid 'result': %s", p); ret = DUO_SERVER_ERROR; } } else { ret = DUO_CONTINUE; } _JSON_VALUE_FREE(json); return (ret); } duo_code_t _duo_prompt(struct duo_ctx *ctx, int flags, char *buf, size_t sz, char *p, size_t sp) { char *pos, *passcode; passcode = getenv(DUO_ENV_VAR_NAME); if ((flags & DUO_FLAG_ENV) && (passcode != NULL)) { if (strlcpy(p, passcode, sp) >= sp) { return (DUO_LIB_ERROR); } if (ctx->conv_status != NULL) { ctx->conv_status(ctx->conv_arg, ENV_VAR_MSG); } return (DUO_CONTINUE); } else if ((flags & DUO_FLAG_AUTO) != 0) { /* Find default OOB factor for automatic login */ JSON_Value *json = json_parse_string(ctx->body); JSON_Object *json_obj = json_value_get_object(json); JSON_Object *response; JSON_Object *factors; _JSON_FIND_OBJECT(response, json_obj, "response", json); _JSON_FIND_OBJECT(factors, response, "factors", json); const char* default_factor; _JSON_FIND_STRING(default_factor, factors, "default", json); if (ctx->conv_status) { if ((pos = strstr(default_factor, "push"))) { ctx->conv_status(ctx->conv_arg, AUTOPUSH_MSG); } else if ((pos = strstr(default_factor, "phone"))) { ctx->conv_status(ctx->conv_arg, AUTOPHONE_MSG); } else { ctx->conv_status(ctx->conv_arg, AUTODEFAULT_MSG); } } if (strlcpy(p, default_factor, sp) >= sp) { _JSON_VALUE_FREE(json); return (DUO_LIB_ERROR); } else { _JSON_VALUE_FREE(json); return (DUO_CONTINUE); } } else { /* Prompt user for factor choice / token */ if (ctx->conv_prompt == NULL) { _duo_seterr(ctx, "No prompt function set"); return (DUO_CLIENT_ERROR); } JSON_Value *json = json_parse_string(ctx->body); JSON_Object *json_obj = json_value_get_object(json); JSON_Object *response; _JSON_FIND_OBJECT(response, json_obj, "response", json); const char* prompt; _JSON_FIND_STRING(prompt, response, "prompt", json); if (ctx->conv_prompt(ctx->conv_arg, prompt, buf, sz) == NULL) { _duo_seterr(ctx, "Error gathering user response"); _JSON_VALUE_FREE(json); return (DUO_ABORT); } strtok(buf, "\r\n"); JSON_Object *factors; _JSON_FIND_OBJECT(factors, response, "factors", json); // buf might not exist in factors JSON_Object, like if the user input // a passcode const char *factor_str = json_object_get_string(factors, buf); if (factor_str == NULL) { factor_str = buf; } if (strlcpy(p, factor_str, sp) >= sp) { _JSON_VALUE_FREE(json); return (DUO_LIB_ERROR); } _JSON_VALUE_FREE(json); 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) { duo_code_t ret; char buf[256]; char *pushinfo = NULL; char p[256]; 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, username, client_ip, failmode)) != 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, flags, buf, sizeof(buf), p, sizeof(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.json", flags & DUO_FLAG_SYNC ? DUO_NO_TIMEOUT : ctx->https_timeout)) != DUO_OK || (ret = _duo_json_response(ctx)) != DUO_OK) { return (ret); } /* Handle sync status */ if ((flags & DUO_FLAG_SYNC) != 0) { JSON_Value *json = json_parse_string(ctx->body); JSON_Object *json_obj = json_value_get_object(json); JSON_Object *json_response; _JSON_FIND_OBJECT(json_response, json_obj, "response", json); const char *status; _JSON_FIND_STRING(status, json_response, "status", json); if (ctx->conv_status != NULL) { ctx->conv_status(ctx->conv_arg, status); } const char* result; _JSON_FIND_STRING(result, json_response, "result", json); if (strcasecmp(result, "allow") == 0) { ret = DUO_OK; } else if (strcasecmp(result, "deny") == 0) { ret = DUO_FAIL; } else { _duo_seterr(ctx, "JSON invalid 'result': %s", result); ret = DUO_SERVER_ERROR; } _JSON_VALUE_FREE(json); return (ret); } /* Async status - long-poll on txid */ JSON_Value *json = json_parse_string(ctx->body); JSON_Object *json_obj = json_value_get_object(json); JSON_Object *json_response; _JSON_FIND_OBJECT(json_response, json_obj, "response", json); const char* txid; _JSON_FIND_STRING(txid, json_response, "txid", json); if (strlcpy(buf, txid, sizeof(buf)) >= sizeof(buf)) { _JSON_VALUE_FREE(json); 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.json", DUO_NO_TIMEOUT)) != DUO_OK || (ret = _duo_json_response(ctx)) != DUO_OK) { break; } JSON_Value *json_new = json_parse_string(ctx->body); JSON_Object *json_obj_new = json_value_get_object(json_new); JSON_Object *json_response_new; _JSON_FIND_OBJECT(json_response_new, json_obj_new, "response", json); const char *status_json_obj; _JSON_FIND_STRING(status_json_obj, json_response_new, "status", json); if (status_json_obj != NULL) { if (ctx->conv_status != NULL) { ctx->conv_status(ctx->conv_arg, status_json_obj); } } //We might not have 'result' defined but we don't want to quit the program //if it's not in our object yet const char* result = json_object_get_string(json_response_new, "result"); if (result != NULL) { if (strcasecmp(result, "allow") == 0) { ret = DUO_OK; } else if (strcasecmp(result, "deny") == 0) { ret = DUO_FAIL; } else { _duo_seterr(ctx, "JSON invalid 'result': %s", result); ret = DUO_SERVER_ERROR; } break; } } _JSON_VALUE_FREE(json); return (ret); }