/* request-key.c: hand a key request off to the appropriate process * * Copyright (C) 2005 Red Hat, Inc. All Rights Reserved. * Written by David Howells (dhowells@redhat.com) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. * * /sbin/request-key [] * * Searches the specified session ring for a key indicating the command to run: * type: "user" * desc: "request-key:" * data: command name, e.g.: "/home/dhowells/request-key-create.sh" */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "keyutils.h" struct parameters { key_serial_t key_id; char *op; char *key_type; char *key_desc; char *callout_info; char *key; char *uid; char *gid; char *thread_keyring; char *process_keyring; char *session_keyring; int len; int oplen; int ktlen; int kdlen; int cilen; }; static int verbosity; static int xlocaldirs; static int xnolog; static int debug_mode; static char conffile[PATH_MAX + 1]; static int confline; static int norecurse; static char cmd[4096 + 2], cmd_conffile[PATH_MAX + 1]; static unsigned int cmd_wildness[4] = { UINT_MAX, UINT_MAX, UINT_MAX, UINT_MAX }; static int cmd_len, cmd_confline; static void lookup_action(struct parameters *params) __attribute__((noreturn)); static void scan_conf_dir(struct parameters *params, const char *confdir); static void scan_conf_file(struct parameters *params, int dirfd, const char *conffile); static void execute_program(struct parameters *params, char *cmdline) __attribute__((noreturn)); static void pipe_to_program(struct parameters *params, char *prog, char **argv) __attribute__((noreturn)); static int match(const char *pattern, int plen, const char *datum, int dlen, unsigned int *wildness); static void debug(const char *fmt, ...) __attribute__((format(printf, 1, 2))); static void debug(const char *fmt, ...) { va_list va; if (verbosity) { va_start(va, fmt); vfprintf(stderr, fmt, va); va_end(va); if (!xnolog) { openlog("request-key", 0, LOG_AUTHPRIV); va_start(va, fmt); vsyslog(LOG_DEBUG, fmt, va); va_end(va); closelog(); } } } static void error(const char *fmt, ...) __attribute__((noreturn, format(printf, 1, 2))); static void error(const char *fmt, ...) { va_list va; if (verbosity) { va_start(va, fmt); vfprintf(stderr, fmt, va); va_end(va); } if (!xnolog) { openlog("request-key", 0, LOG_AUTHPRIV); va_start(va, fmt); vsyslog(LOG_ERR, fmt, va); va_end(va); closelog(); } exit(1); } #define file_error(FMT, ...) error("%s: "FMT, conffile, ## __VA_ARGS__) #define line_error(FMT, ...) error("%s:%d: "FMT, conffile, confline, ## __VA_ARGS__) static void oops(int x) { error("Died on signal %d", x); } /*****************************************************************************/ /* * */ int main(int argc, char *argv[]) { struct parameters params; char *test_desc = "user;0;0;1f0000;debug:1234"; char *buf; int ret, ntype, dpos, n, fd, opt; if (argc == 2 && strcmp(argv[1], "--version") == 0) { printf("request-key from %s (Built %s)\n", keyutils_version_string, keyutils_build_string); return 0; } signal(SIGSEGV, oops); signal(SIGBUS, oops); signal(SIGPIPE, SIG_IGN); while (opt = getopt(argc, argv, "D:dlnv"), opt != -1) { switch (opt) { case 'D': test_desc = optarg; break; case 'd': debug_mode = 1; break; case 'l': xlocaldirs = 1; break; case 'n': xnolog = 1; break; case 'v': verbosity++; break; } } argc -= optind; argv += optind; if (argc != 7 && argc != 8) error("Unexpected argument count: %d\n", argc); fd = open("/dev/null", O_RDWR); if (fd < 0) error("open"); if (fd > 2) { close(fd); } else if (fd < 2) { ret = dup(fd); if (ret < 0) error("dup failed: %m\n"); if (ret < 2 && dup(fd) < 0) error("dup failed: %m\n"); } params.op = argv[0]; params.key = argv[1]; params.uid = argv[2]; params.gid = argv[3]; params.thread_keyring = argv[4]; params.process_keyring = argv[5]; params.session_keyring = argv[6]; params.callout_info = argv[7]; params.key_id = atoi(params.key); /* assume authority over the key * - older kernel doesn't support this function */ if (!debug_mode) { ret = keyctl_assume_authority(params.key_id); if (ret < 0 && !(argc == 8 || errno == EOPNOTSUPP)) error("Failed to assume authority over key %d (%m)\n", params.key_id); } /* ask the kernel to describe the key to us */ if (!debug_mode) { ret = keyctl_describe_alloc(params.key_id, &buf); if (ret < 0) goto inaccessible; } else { buf = strdup(test_desc); } /* extract the type and description from the key */ debug("Key descriptor: \"%s\"\n", buf); ntype = -1; dpos = -1; n = sscanf(buf, "%*[^;]%n;%*d;%*d;%x;%n", &ntype, &n, &dpos); if (n != 1) error("Failed to parse key description\n"); params.key_type = buf; params.key_type[ntype] = 0; params.key_desc = buf + dpos; debug("Key type: %s\n", params.key_type); debug("Key desc: %s\n", params.key_desc); /* get hold of the callout info */ if (!params.callout_info) { void *tmp; if (keyctl_read_alloc(KEY_SPEC_REQKEY_AUTH_KEY, &tmp) < 0) error("Failed to retrieve callout info (%m)\n"); params.callout_info = tmp; } debug("CALLOUT: '%s'\n", params.callout_info); /* determine the action to perform */ params.oplen = strlen(params.op); params.ktlen = strlen(params.key_type); params.kdlen = strlen(params.key_desc); params.cilen = strlen(params.callout_info); lookup_action(¶ms); inaccessible: error("Key %d is inaccessible (%m)\n", params.key_id); } /* end main() */ /*****************************************************************************/ /* * determine the action to perform */ static void lookup_action(struct parameters *params) { if (!xlocaldirs) { scan_conf_dir(params, "/etc/request-key.d"); scan_conf_file(params, AT_FDCWD, "/etc/request-key.conf"); } else { scan_conf_dir(params, "request-key.d"); scan_conf_file(params, AT_FDCWD, "request-key.conf"); } if (cmd_len > 0) execute_program(params, cmd); file_error("No matching action\n"); } /*****************************************************************************/ /* * Scan the files in a configuration directory. */ static void scan_conf_dir(struct parameters *params, const char *confdir) { struct dirent *d; DIR *dir; int l; debug("__ SCAN %s __\n", confdir); dir = opendir(confdir); if (!dir) { if (errno == ENOENT) return; error("Cannot open %s: %m\n", confdir); } while ((d = readdir(dir))) { if (d->d_name[0] == '.') continue; if (d->d_type != DT_UNKNOWN && d->d_type != DT_REG) continue; l = strlen(d->d_name); if (l < 5) continue; if (memcmp(d->d_name + l - 5, ".conf", 5) != 0) continue; scan_conf_file(params, dirfd(dir), d->d_name); } closedir(dir); } /*****************************************************************************/ /* * Scan the contents of a configuration file. */ static void scan_conf_file(struct parameters *params, int dirfd, const char *conffile) { char buf[4096 + 2], *p, *q; FILE *conf; int fd; debug("__ read %s __\n", conffile); fd = openat(dirfd, conffile, O_RDONLY); if (fd < 0) { if (errno == ENOENT) return; error("Cannot open %s: %m\n", conffile); } conf = fdopen(fd, "r"); if (!conf) error("Cannot open %s: %m\n", conffile); for (confline = 1;; confline++) { unsigned int wildness[4] = {}; unsigned int len; /* read the file line-by-line */ if (!fgets(buf, sizeof(buf), conf)) { if (feof(conf)) break; file_error("error %m\n"); } len = strlen(buf); if (len >= sizeof(buf) - 2) line_error("Line too long\n"); /* ignore blank lines and comments */ if (len == 1 || buf[0] == '#' || isspace(buf[0])) continue; buf[--len] = 0; p = buf; /* attempt to match the op */ q = p; while (*p && !isspace(*p)) p++; if (!*p) goto syntax_error; *p = 0; if (!match(q, p - q, params->op, params->oplen, &wildness[0])) continue; p++; /* attempt to match the type */ while (isspace(*p)) p++; if (!*p) goto syntax_error; q = p; while (*p && !isspace(*p)) p++; if (!*p) goto syntax_error; *p = 0; if (!match(q, p - q, params->key_type, params->ktlen, &wildness[1])) continue; p++; /* attempt to match the description */ while (isspace(*p)) p++; if (!*p) goto syntax_error; q = p; while (*p && !isspace(*p)) p++; if (!*p) goto syntax_error; *p = 0; if (!match(q, p - q, params->key_desc, params->kdlen, &wildness[2])) continue; p++; /* attempt to match the callout info */ while (isspace(*p)) p++; if (!*p) goto syntax_error; q = p; while (*p && !isspace(*p)) p++; if (!*p) goto syntax_error; *p = 0; if (!match(q, p - q, params->callout_info, params->cilen, &wildness[3])) continue; p++; /* we've got a match */ while (isspace(*p)) p++; if (!*p) goto syntax_error; debug("%s:%d: Line matches '%s' (%u,%u,%u,%u)\n", conffile, confline, p, wildness[0], wildness[1], wildness[2], wildness[3]); if (wildness[0] < cmd_wildness[0] || (wildness[0] == cmd_wildness[0] && wildness[1] < cmd_wildness[1]) || (wildness[0] == cmd_wildness[0] && wildness[1] == cmd_wildness[1] && wildness[2] < cmd_wildness[2]) || (wildness[0] == cmd_wildness[0] && wildness[1] == cmd_wildness[1] && wildness[2] == cmd_wildness[2] && wildness[3] < cmd_wildness[3]) ) { memcpy(cmd_wildness, wildness, sizeof(cmd_wildness)); cmd_len = len - (p - buf); cmd_confline = confline; debug("%s:%d: Prefer command (%u,%u,%u,%u)\n", conffile, confline, wildness[0], wildness[1], wildness[2], wildness[3]); memcpy(cmd, p, cmd_len + 1); strcpy(cmd_conffile, conffile); } } fclose(conf); return; syntax_error: line_error("Syntax error\n"); } /*****************************************************************************/ /* * attempt to match a datum to a pattern * - one asterisk is allowed anywhere in the pattern to indicate a wildcard * - returns true if matched, false if not * - adds the total number of chars skipped by wildcard to *_wildness */ static int match(const char *pattern, int plen, const char *datum, int dlen, unsigned int *_wildness) { const char *asterisk; int n; if (verbosity >= 2) debug("match(%*.*s,%*.*s)", plen, plen, pattern, dlen, dlen, datum); asterisk = memchr(pattern, '*', plen); if (!asterisk) { /* exact match only if no wildcard */ if (plen == dlen && memcmp(pattern, datum, dlen) == 0) goto yes; goto no; } /* the datum mustn't be shorter than the pattern without the asterisk */ if (dlen < plen - 1) goto no; n = asterisk - pattern; if (n == 0) { /* wildcard at beginning of pattern */ pattern++; if (!*pattern) goto yes_wildcard; /* "*" matches everything */ /* match the end of the datum */ if (memcmp(pattern, datum + (dlen - (plen - 1)), plen - 1) == 0) goto yes_wildcard; goto no; } /* need to match beginning of datum for "abc*" and "abc*def" */ if (memcmp(pattern, datum, n) != 0) goto no; if (!asterisk[1]) goto yes_wildcard; /* "abc*" matches */ /* match the end of the datum */ asterisk++; n = plen - n - 1; if (memcmp(pattern, datum + (dlen - n), n) == 0) goto yes_wildcard; no: if (verbosity >= 2) debug(" = no\n"); return 0; yes: if (verbosity >= 2) debug(" = yes (w=0)\n"); return 1; yes_wildcard: *_wildness += dlen - (plen - 1); if (verbosity >= 2) debug(" = yes (w=%u)\n", dlen - (plen - 1)); return 1; } /* end match() */ /*****************************************************************************/ /* * execute a program to deal with a key */ static void execute_program(struct parameters *params, char *cmdline) { char *argv[256]; char *prog, *p, *q; int argc, pipeit; debug("execute_program('%s','%s')\n", params->callout_info, cmdline); /* if the commandline begins with a bar, then we pipe the callout data into it and read * back the payload data */ pipeit = 0; if (cmdline[0] == '|') { pipeit = 1; cmdline++; } /* extract the path to the program to run */ prog = p = cmdline; while (*p && !isspace(*p)) p++; // if (!*p) // line_error("No command path\n"); // *p++ = 0; if (*p) *p++ = 0; argv[0] = strrchr(prog, '/') + 1; /* extract the arguments */ for (argc = 1; p; argc++) { while (isspace(*p)) p++; if (!*p) break; if (argc >= 254) line_error("Too many arguments\n"); argv[argc] = q = p; while (*p && !isspace(*p)) p++; if (*p) *p++ = 0; else p = NULL; debug("argv[%d]: '%s'\n", argc, argv[argc]); if (*q != '%') continue; /* it's a macro */ q++; if (!*q) line_error("Missing macro name\n"); if (*q == '%') { /* it's actually an anti-macro escape "%%..." -> "%..." */ argv[argc]++; continue; } /* single character macros */ if (!q[1]) { switch (*q) { case 'o': argv[argc] = params->op; continue; case 'k': argv[argc] = params->key; continue; case 't': argv[argc] = params->key_type; continue; case 'd': argv[argc] = params->key_desc; continue; case 'c': argv[argc] = params->callout_info; continue; case 'u': argv[argc] = params->uid; continue; case 'g': argv[argc] = params->gid; continue; case 'T': argv[argc] = params->thread_keyring; continue; case 'P': argv[argc] = params->process_keyring; continue; case 'S': argv[argc] = params->session_keyring; continue; default: line_error("Unsupported macro\n"); } } /* keysub macro */ if (*q == '{') { key_serial_t keysub; void *tmp; char *ksdesc, *end, *subdata; int ret, loop; /* extract type and description */ q++; ksdesc = strchr(q, ':'); if (!ksdesc) line_error("Keysub macro lacks ':'\n"); *ksdesc++ = 0; end = strchr(ksdesc, '}'); if (!end) line_error("Unterminated keysub macro\n"); *end++ = 0; if (*end) line_error("Keysub macro has trailing rubbish\n"); debug("Keysub: %s key \"%s\"\n", q, ksdesc); if (!q[0]) line_error("Keysub type empty\n"); if (!ksdesc[0]) line_error("Keysub description empty\n"); /* look up the key in the requestor's keyrings, but fail immediately if the * key is not found rather than invoking /sbin/request-key again */ keysub = request_key(q, ksdesc, NULL, 0); if (keysub < 0) line_error("Keysub key not found: %m\n"); ret = keyctl_read_alloc(keysub, &tmp); if (ret < 0) line_error("Can't read keysub %d data: %m\n", keysub); subdata = tmp; for (loop = 0; loop < ret; loop++) if (!isprint(subdata[loop])) error("keysub %d data not printable ('%02hhx')\n", keysub, subdata[loop]); argv[argc] = subdata; continue; } } if (argc == 0) line_error("No arguments\n"); argv[argc] = NULL; if (verbosity) { char **ap; debug("%s %s\n", pipeit ? "PipeThru" : "Run", prog); for (ap = argv; *ap; ap++) debug("- argv[%td] = \"%s\"\n", ap - argv, *ap); } /* become the same UID/GID as the key requesting process */ //setgid(atoi(xuid)); //setuid(atoi(xgid)); /* if the last argument is a single bar, we spawn off the program dangling on the end of * three pipes and read the key material from the program, otherwise we just exec */ if (debug_mode) { printf("-- exec disabled --\n"); exit(0); } if (pipeit) pipe_to_program(params, prog, argv); /* attempt to execute the command */ execv(prog, argv); line_error("Failed to execute '%s': %m\n", prog); } /* end execute_program() */ /*****************************************************************************/ /* * pipe the callout information to the specified program and retrieve the * payload data over another pipe */ static void pipe_to_program(struct parameters *params, char *prog, char **argv) { char errbuf[512], payload[32768 + 1], *pp, *pc, *pe; int ipi[2], opi[2], epi[2], childpid; int ifl, ofl, efl, npay, ninfo, espace, tmp; debug("pipe_to_program(%s -> %s)", params->callout_info, prog); if (pipe(ipi) < 0 || pipe(opi) < 0 || pipe(epi) < 0) error("pipe failed: %m"); childpid = fork(); if (childpid == -1) error("fork failed: %m"); if (childpid == 0) { /* child process */ if (dup2(ipi[0], 0) < 0 || dup2(opi[1], 1) < 0 || dup2(epi[1], 2) < 0) error("dup2 failed: %m"); close(ipi[0]); close(ipi[1]); close(opi[0]); close(opi[1]); close(epi[0]); close(epi[1]); execv(prog, argv); line_error("Failed to execute '%s': %m\n", prog); } /* parent process */ close(ipi[0]); close(opi[1]); close(epi[1]); #define TOSTDIN ipi[1] #define FROMSTDOUT opi[0] #define FROMSTDERR epi[0] ifl = fcntl(TOSTDIN, F_GETFL); ofl = fcntl(FROMSTDOUT, F_GETFL); efl = fcntl(FROMSTDERR, F_GETFL); if (ifl < 0 || ofl < 0 || efl < 0) error("fcntl/F_GETFL failed: %m"); ifl |= O_NONBLOCK; ofl |= O_NONBLOCK; efl |= O_NONBLOCK; if (fcntl(TOSTDIN, F_SETFL, ifl) < 0 || fcntl(FROMSTDOUT, F_SETFL, ofl) < 0 || fcntl(FROMSTDERR, F_SETFL, efl) < 0) error("fcntl/F_SETFL failed: %m"); ninfo = params->cilen; pc = params->callout_info; npay = sizeof(payload); pp = payload; espace = sizeof(errbuf); pe = errbuf; do { fd_set rfds, wfds; FD_ZERO(&rfds); FD_ZERO(&wfds); if (TOSTDIN != -1) { if (ninfo > 0) { FD_SET(TOSTDIN, &wfds); } else { close(TOSTDIN); TOSTDIN = -1; continue; } } if (FROMSTDOUT != -1) FD_SET(FROMSTDOUT, &rfds); if (FROMSTDERR != -1) FD_SET(FROMSTDERR, &rfds); tmp = TOSTDIN > FROMSTDOUT ? TOSTDIN : FROMSTDOUT; tmp = tmp > FROMSTDERR ? tmp : FROMSTDERR; tmp++; debug("select r=%d,%d w=%d m=%d\n", FROMSTDOUT, FROMSTDERR, TOSTDIN, tmp); tmp = select(tmp, &rfds, &wfds, NULL, NULL); if (tmp < 0) error("select failed: %m\n"); if (TOSTDIN != -1 && FD_ISSET(TOSTDIN, &wfds)) { tmp = write(TOSTDIN, pc, ninfo); if (tmp < 0) { if (errno != EPIPE) error("write failed: %m\n"); debug("EPIPE"); ninfo = 0; } else { debug("wrote %d\n", tmp); pc += tmp; ninfo -= tmp; } } if (FROMSTDOUT != -1 && FD_ISSET(FROMSTDOUT, &rfds)) { tmp = read(FROMSTDOUT, pp, npay); if (tmp < 0) error("read failed: %m\n"); debug("read %d\n", tmp); if (tmp == 0) { close(FROMSTDOUT); FROMSTDOUT = -1; } else { pp += tmp; npay -= tmp; if (npay == 0) error("Too much data read from query program\n"); } } if (FROMSTDERR != -1 && FD_ISSET(FROMSTDERR, &rfds)) { char *nl; tmp = read(FROMSTDERR, pe, espace); if (tmp < 0) error("read failed: %m\n"); debug("read err %d\n", tmp); if (tmp == 0) { close(FROMSTDERR); FROMSTDERR = -1; continue; } pe += tmp; espace -= tmp; while ((nl = memchr(errbuf, '\n', pe - errbuf))) { int n, rest; nl++; n = nl - errbuf; if (verbosity) fprintf(stderr, "Child: %*.*s", n, n, errbuf); if (!xnolog) { openlog("request-key", 0, LOG_AUTHPRIV); syslog(LOG_ERR, "Child: %*.*s", n, n, errbuf); closelog(); } rest = pe - nl; if (rest > 0) { memmove(errbuf, nl, rest); pe -= n; espace += n; } else { pe = errbuf; espace = sizeof(errbuf); } } if (espace == 0) { int n = sizeof(errbuf); if (verbosity) fprintf(stderr, "Child: %*.*s", n, n, errbuf); if (!xnolog) { openlog("request-key", 0, LOG_AUTHPRIV); syslog(LOG_ERR, "Child: %*.*s", n, n, errbuf); closelog(); } pe = errbuf; espace = sizeof(errbuf); } } } while (TOSTDIN != -1 || FROMSTDOUT != -1 || FROMSTDERR != -1); /* wait for the program to exit */ if (waitpid(childpid, &tmp, 0) != childpid) error("wait for child failed: %m\n"); /* if the process exited non-zero or died on a signal, then we call back in to ourself to * decide on negation * - this is not exactly beautiful but the quickest way of having configurable negation * settings */ if (WIFEXITED(tmp) && WEXITSTATUS(tmp) != 0) { if (norecurse) error("child exited %d\n", WEXITSTATUS(tmp)); norecurse = 1; debug("child exited %d\n", WEXITSTATUS(tmp)); params->op = "negate"; lookup_action(params); } if (WIFSIGNALED(tmp)) { if (norecurse) error("child died on signal %d\n", WTERMSIG(tmp)); norecurse = 1; params->op = "negate"; lookup_action(params); } /* attempt to instantiate the key */ debug("instantiate with %td bytes\n", pp - payload); if (keyctl_instantiate(params->key_id, payload, pp - payload, 0) < 0) error("instantiate key failed: %m\n"); debug("instantiation successful\n"); exit(0); } /* end pipe_to_program() */