summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordjm@openbsd.org <djm@openbsd.org>2015-05-21 06:38:35 +0000
committerDamien Miller <djm@mindrot.org>2015-05-21 16:44:56 +1000
commit24232a3e5ab467678a86aa67968bbb915caffed4 (patch)
treee81f44adced3d0058b6b6e25ca8dc82a3a5db153
parentd80fbe41a57c72420c87a628444da16d09d66ca7 (diff)
upstream commit
support arguments to AuthorizedKeysCommand bz#2081 loosely based on patch by Sami Hartikainen feedback and ok markus@ Upstream-ID: b080387a14aa67dddd8ece67c00f268d626541f7
-rw-r--r--auth2-pubkey.c464
-rw-r--r--sshd_config.522
2 files changed, 378 insertions, 108 deletions
diff --git a/auth2-pubkey.c b/auth2-pubkey.c
index f96e843c2..553144c7b 100644
--- a/auth2-pubkey.c
+++ b/auth2-pubkey.c
@@ -1,4 +1,4 @@
1/* $OpenBSD: auth2-pubkey.c,v 1.49 2015/05/04 06:10:48 djm Exp $ */ 1/* $OpenBSD: auth2-pubkey.c,v 1.50 2015/05/21 06:38:35 djm Exp $ */
2/* 2/*
3 * Copyright (c) 2000 Markus Friedl. All rights reserved. 3 * Copyright (c) 2000 Markus Friedl. All rights reserved.
4 * 4 *
@@ -65,6 +65,9 @@
65#include "monitor_wrap.h" 65#include "monitor_wrap.h"
66#include "authfile.h" 66#include "authfile.h"
67#include "match.h" 67#include "match.h"
68#include "ssherr.h"
69#include "channels.h" /* XXX for session.h */
70#include "session.h" /* XXX for child_set_env(); refactor? */
68 71
69/* import */ 72/* import */
70extern ServerOptions options; 73extern ServerOptions options;
@@ -248,6 +251,288 @@ pubkey_auth_info(Authctxt *authctxt, const Key *key, const char *fmt, ...)
248 free(extra); 251 free(extra);
249} 252}
250 253
254/*
255 * Splits 's' into an argument vector. Handles quoted string and basic
256 * escape characters (\\, \", \'). Caller must free the argument vector
257 * and its members.
258 */
259static int
260split_argv(const char *s, int *argcp, char ***argvp)
261{
262 int r = SSH_ERR_INTERNAL_ERROR;
263 int argc = 0, quote, i, j;
264 char *arg, **argv = xcalloc(1, sizeof(*argv));
265
266 *argvp = NULL;
267 *argcp = 0;
268
269 for (i = 0; s[i] != '\0'; i++) {
270 /* Skip leading whitespace */
271 if (s[i] == ' ' || s[i] == '\t')
272 continue;
273
274 /* Start of a token */
275 quote = 0;
276 if (s[i] == '\\' &&
277 (s[i + 1] == '\'' || s[i + 1] == '\"' || s[i + 1] == '\\'))
278 i++;
279 else if (s[i] == '\'' || s[i] == '"')
280 quote = s[i++];
281
282 argv = xreallocarray(argv, (argc + 2), sizeof(*argv));
283 arg = argv[argc++] = xcalloc(1, strlen(s + i) + 1);
284 argv[argc] = NULL;
285
286 /* Copy the token in, removing escapes */
287 for (j = 0; s[i] != '\0'; i++) {
288 if (s[i] == '\\') {
289 if (s[i + 1] == '\'' ||
290 s[i + 1] == '\"' ||
291 s[i + 1] == '\\') {
292 i++; /* Skip '\' */
293 arg[j++] = s[i];
294 } else {
295 /* Unrecognised escape */
296 arg[j++] = s[i];
297 }
298 } else if (quote == 0 && (s[i] == ' ' || s[i] == '\t'))
299 break; /* done */
300 else if (quote != 0 && s[i] == quote)
301 break; /* done */
302 else
303 arg[j++] = s[i];
304 }
305 if (s[i] == '\0') {
306 if (quote != 0) {
307 /* Ran out of string looking for close quote */
308 r = SSH_ERR_INVALID_FORMAT;
309 goto out;
310 }
311 break;
312 }
313 }
314 /* Success */
315 *argcp = argc;
316 *argvp = argv;
317 argc = 0;
318 argv = NULL;
319 r = 0;
320 out:
321 if (argc != 0 && argv != NULL) {
322 for (i = 0; i < argc; i++)
323 free(argv[i]);
324 free(argv);
325 }
326 return r;
327}
328
329/*
330 * Reassemble an argument vector into a string, quoting and escaping as
331 * necessary. Caller must free returned string.
332 */
333static char *
334assemble_argv(int argc, char **argv)
335{
336 int i, j, ws, r;
337 char c, *ret;
338 struct sshbuf *buf, *arg;
339
340 if ((buf = sshbuf_new()) == NULL || (arg = sshbuf_new()) == NULL)
341 fatal("%s: sshbuf_new failed", __func__);
342
343 for (i = 0; i < argc; i++) {
344 ws = 0;
345 sshbuf_reset(arg);
346 for (j = 0; argv[i][j] != '\0'; j++) {
347 r = 0;
348 c = argv[i][j];
349 switch (c) {
350 case ' ':
351 case '\t':
352 ws = 1;
353 r = sshbuf_put_u8(arg, c);
354 break;
355 case '\\':
356 case '\'':
357 case '"':
358 if ((r = sshbuf_put_u8(arg, '\\')) != 0)
359 break;
360 /* FALLTHROUGH */
361 default:
362 r = sshbuf_put_u8(arg, c);
363 break;
364 }
365 if (r != 0)
366 fatal("%s: sshbuf_put_u8: %s",
367 __func__, ssh_err(r));
368 }
369 if ((i != 0 && (r = sshbuf_put_u8(buf, ' ')) != 0) ||
370 (ws != 0 && (r = sshbuf_put_u8(buf, '"')) != 0) ||
371 (r = sshbuf_putb(buf, arg)) != 0 ||
372 (ws != 0 && (r = sshbuf_put_u8(buf, '"')) != 0))
373 fatal("%s: buffer error: %s", __func__, ssh_err(r));
374 }
375 if ((ret = malloc(sshbuf_len(buf) + 1)) == NULL)
376 fatal("%s: malloc failed", __func__);
377 memcpy(ret, sshbuf_ptr(buf), sshbuf_len(buf));
378 ret[sshbuf_len(buf)] = '\0';
379 sshbuf_free(buf);
380 sshbuf_free(arg);
381 return ret;
382}
383
384/*
385 * Runs command in a subprocess. Returns pid on success and a FILE* to the
386 * subprocess' stdout or 0 on failure.
387 * NB. "command" is only used for logging.
388 */
389static pid_t
390subprocess(const char *tag, struct passwd *pw, const char *command,
391 int ac, char **av, FILE **child)
392{
393 FILE *f;
394 struct stat st;
395 int devnull, p[2], i;
396 pid_t pid;
397 char *cp, errmsg[512];
398 u_int envsize;
399 char **child_env;
400
401 *child = NULL;
402
403 debug3("%s: %s command \"%s\" running as %s", __func__,
404 tag, command, pw->pw_name);
405
406 /* Verify the path exists and is safe-ish to execute */
407 if (*av[0] != '/') {
408 error("%s path is not absolute", tag);
409 return 0;
410 }
411 temporarily_use_uid(pw);
412 if (stat(av[0], &st) < 0) {
413 error("Could not stat %s \"%s\": %s", tag,
414 av[0], strerror(errno));
415 restore_uid();
416 return 0;
417 }
418 if (auth_secure_path(av[0], &st, NULL, 0,
419 errmsg, sizeof(errmsg)) != 0) {
420 error("Unsafe %s \"%s\": %s", tag, av[0], errmsg);
421 restore_uid();
422 return 0;
423 }
424
425 /*
426 * Run the command; stderr is left in place, stdout is the
427 * authorized_keys output.
428 */
429 if (pipe(p) != 0) {
430 error("%s: pipe: %s", tag, strerror(errno));
431 restore_uid();
432 return 0;
433 }
434
435 /*
436 * Don't want to call this in the child, where it can fatal() and
437 * run cleanup_exit() code.
438 */
439 restore_uid();
440
441 switch ((pid = fork())) {
442 case -1: /* error */
443 error("%s: fork: %s", tag, strerror(errno));
444 close(p[0]);
445 close(p[1]);
446 return 0;
447 case 0: /* child */
448 /* Prepare a minimal environment for the child. */
449 envsize = 5;
450 child_env = xcalloc(sizeof(*child_env), envsize);
451 child_set_env(&child_env, &envsize, "PATH", _PATH_STDPATH);
452 child_set_env(&child_env, &envsize, "USER", pw->pw_name);
453 child_set_env(&child_env, &envsize, "LOGNAME", pw->pw_name);
454 child_set_env(&child_env, &envsize, "HOME", pw->pw_dir);
455 if ((cp = getenv("LANG")) != NULL)
456 child_set_env(&child_env, &envsize, "LANG", cp);
457
458 for (i = 0; i < NSIG; i++)
459 signal(i, SIG_DFL);
460
461 if ((devnull = open(_PATH_DEVNULL, O_RDWR)) == -1) {
462 error("%s: open %s: %s", tag, _PATH_DEVNULL,
463 strerror(errno));
464 _exit(1);
465 }
466 /* Keep stderr around a while longer to catch errors */
467 if (dup2(devnull, STDIN_FILENO) == -1 ||
468 dup2(p[1], STDOUT_FILENO) == -1) {
469 error("%s: dup2: %s", tag, strerror(errno));
470 _exit(1);
471 }
472 closefrom(STDERR_FILENO + 1);
473
474 /* Don't use permanently_set_uid() here to avoid fatal() */
475 if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) != 0) {
476 error("%s: setresgid %u: %s", tag, (u_int)pw->pw_gid,
477 strerror(errno));
478 _exit(1);
479 }
480 if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) != 0) {
481 error("%s: setresuid %u: %s", tag, (u_int)pw->pw_uid,
482 strerror(errno));
483 _exit(1);
484 }
485 /* stdin is pointed to /dev/null at this point */
486 if (dup2(STDIN_FILENO, STDERR_FILENO) == -1) {
487 error("%s: dup2: %s", tag, strerror(errno));
488 _exit(1);
489 }
490
491 execve(av[0], av, child_env);
492 error("%s exec \"%s\": %s", tag, command, strerror(errno));
493 _exit(127);
494 default: /* parent */
495 break;
496 }
497
498 close(p[1]);
499 if ((f = fdopen(p[0], "r")) == NULL) {
500 error("%s: fdopen: %s", tag, strerror(errno));
501 close(p[0]);
502 /* Don't leave zombie child */
503 kill(pid, SIGTERM);
504 while (waitpid(pid, NULL, 0) == -1 && errno == EINTR)
505 ;
506 return 0;
507 }
508 /* Success */
509 debug3("%s: %s pid %ld", __func__, tag, (long)pid);
510 *child = f;
511 return pid;
512}
513
514/* Returns 0 if pid exited cleanly, non-zero otherwise */
515static int
516exited_cleanly(pid_t pid, const char *tag, const char *cmd)
517{
518 int status;
519
520 while (waitpid(pid, &status, 0) == -1) {
521 if (errno != EINTR) {
522 error("%s: waitpid: %s", tag, strerror(errno));
523 return -1;
524 }
525 }
526 if (WIFSIGNALED(status)) {
527 error("%s %s exited on signal %d", tag, cmd, WTERMSIG(status));
528 return -1;
529 } else if (WEXITSTATUS(status) != 0) {
530 error("%s %s failed, status %d", tag, cmd, WEXITSTATUS(status));
531 return -1;
532 }
533 return 0;
534}
535
251static int 536static int
252match_principals_option(const char *principal_list, struct sshkey_cert *cert) 537match_principals_option(const char *principal_list, struct sshkey_cert *cert)
253{ 538{
@@ -526,144 +811,117 @@ user_key_allowed2(struct passwd *pw, Key *key, char *file)
526static int 811static int
527user_key_command_allowed2(struct passwd *user_pw, Key *key) 812user_key_command_allowed2(struct passwd *user_pw, Key *key)
528{ 813{
529 FILE *f; 814 FILE *f = NULL;
530 int ok, found_key = 0; 815 int r, ok, found_key = 0;
531 struct passwd *pw; 816 struct passwd *pw;
532 struct stat st; 817 int i, uid_swapped = 0, ac = 0;
533 int status, devnull, p[2], i;
534 pid_t pid; 818 pid_t pid;
535 char *username, errmsg[512]; 819 char *username = NULL, *key_fp = NULL, *keytext = NULL;
820 char *tmp, *command = NULL, **av = NULL;
821 void (*osigchld)(int);
536 822
537 if (options.authorized_keys_command == NULL || 823 if (options.authorized_keys_command == NULL)
538 options.authorized_keys_command[0] != '/')
539 return 0; 824 return 0;
540
541 if (options.authorized_keys_command_user == NULL) { 825 if (options.authorized_keys_command_user == NULL) {
542 error("No user for AuthorizedKeysCommand specified, skipping"); 826 error("No user for AuthorizedKeysCommand specified, skipping");
543 return 0; 827 return 0;
544 } 828 }
545 829
830 /*
831 * NB. all returns later this function should go via "out" to
832 * ensure the original SIGCHLD handler is restored properly.
833 */
834 osigchld = signal(SIGCHLD, SIG_DFL);
835
836 /* Prepare and verify the user for the command */
546 username = percent_expand(options.authorized_keys_command_user, 837 username = percent_expand(options.authorized_keys_command_user,
547 "u", user_pw->pw_name, (char *)NULL); 838 "u", user_pw->pw_name, (char *)NULL);
548 pw = getpwnam(username); 839 pw = getpwnam(username);
549 if (pw == NULL) { 840 if (pw == NULL) {
550 error("AuthorizedKeysCommandUser \"%s\" not found: %s", 841 error("AuthorizedKeysCommandUser \"%s\" not found: %s",
551 username, strerror(errno)); 842 username, strerror(errno));
552 free(username); 843 goto out;
553 return 0;
554 } 844 }
555 free(username);
556
557 temporarily_use_uid(pw);
558 845
559 if (stat(options.authorized_keys_command, &st) < 0) { 846 /* Prepare AuthorizedKeysCommand */
560 error("Could not stat AuthorizedKeysCommand \"%s\": %s", 847 if ((key_fp = sshkey_fingerprint(key, options.fingerprint_hash,
561 options.authorized_keys_command, strerror(errno)); 848 SSH_FP_DEFAULT)) == NULL) {
849 error("%s: sshkey_fingerprint failed", __func__);
562 goto out; 850 goto out;
563 } 851 }
564 if (auth_secure_path(options.authorized_keys_command, &st, NULL, 0, 852 if ((r = sshkey_to_base64(key, &keytext)) != 0) {
565 errmsg, sizeof(errmsg)) != 0) { 853 error("%s: sshkey_to_base64 failed: %s", __func__, ssh_err(r));
566 error("Unsafe AuthorizedKeysCommand: %s", errmsg);
567 goto out; 854 goto out;
568 } 855 }
569 856
570 if (pipe(p) != 0) { 857 /* Turn the command into an argument vector */
571 error("%s: pipe: %s", __func__, strerror(errno)); 858 if (split_argv(options.authorized_keys_command, &ac, &av) != 0) {
859 error("AuthorizedKeysCommand \"%s\" contains invalid quotes",
860 command);
572 goto out; 861 goto out;
573 } 862 }
574 863 if (ac == 0) {
575 debug3("Running AuthorizedKeysCommand: \"%s %s\" as \"%s\"", 864 error("AuthorizedKeysCommand \"%s\" yielded no arguments",
576 options.authorized_keys_command, user_pw->pw_name, pw->pw_name); 865 command);
866 goto out;
867 }
868 for (i = 1; i < ac; i++) {
869 tmp = percent_expand(av[i],
870 "u", user_pw->pw_name,
871 "h", user_pw->pw_dir,
872 "t", sshkey_ssh_name(key),
873 "f", key_fp,
874 "k", keytext,
875 (char *)NULL);
876 if (tmp == NULL)
877 fatal("%s: percent_expand failed", __func__);
878 free(av[i]);
879 av[i] = tmp;
880 }
881 /* Prepare a printable command for logs, etc. */
882 command = assemble_argv(ac, av);
577 883
578 /* 884 /*
579 * Don't want to call this in the child, where it can fatal() and 885 * If AuthorizedKeysCommand was run without arguments
580 * run cleanup_exit() code. 886 * then fall back to the old behaviour of passing the
887 * target username as a single argument.
581 */ 888 */
582 restore_uid(); 889 if (ac == 1) {
583 890 av = xreallocarray(av, ac + 2, sizeof(*av));
584 switch ((pid = fork())) { 891 av[1] = xstrdup(user_pw->pw_name);
585 case -1: /* error */ 892 av[2] = NULL;
586 error("%s: fork: %s", __func__, strerror(errno)); 893 /* Fix up command too, since it is used in log messages */
587 close(p[0]); 894 free(command);
588 close(p[1]); 895 xasprintf(&command, "%s %s", av[0], av[1]);
589 return 0;
590 case 0: /* child */
591 for (i = 0; i < NSIG; i++)
592 signal(i, SIG_DFL);
593
594 if ((devnull = open(_PATH_DEVNULL, O_RDWR)) == -1) {
595 error("%s: open %s: %s", __func__, _PATH_DEVNULL,
596 strerror(errno));
597 _exit(1);
598 }
599 /* Keep stderr around a while longer to catch errors */
600 if (dup2(devnull, STDIN_FILENO) == -1 ||
601 dup2(p[1], STDOUT_FILENO) == -1) {
602 error("%s: dup2: %s", __func__, strerror(errno));
603 _exit(1);
604 }
605 closefrom(STDERR_FILENO + 1);
606
607 /* Don't use permanently_set_uid() here to avoid fatal() */
608 if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) != 0) {
609 error("setresgid %u: %s", (u_int)pw->pw_gid,
610 strerror(errno));
611 _exit(1);
612 }
613 if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) != 0) {
614 error("setresuid %u: %s", (u_int)pw->pw_uid,
615 strerror(errno));
616 _exit(1);
617 }
618 /* stdin is pointed to /dev/null at this point */
619 if (dup2(STDIN_FILENO, STDERR_FILENO) == -1) {
620 error("%s: dup2: %s", __func__, strerror(errno));
621 _exit(1);
622 }
623
624 execl(options.authorized_keys_command,
625 options.authorized_keys_command, user_pw->pw_name, NULL);
626
627 error("AuthorizedKeysCommand %s exec failed: %s",
628 options.authorized_keys_command, strerror(errno));
629 _exit(127);
630 default: /* parent */
631 break;
632 } 896 }
633 897
898 if ((pid = subprocess("AuthorizedKeysCommand", pw, command,
899 ac, av, &f)) == 0)
900 goto out;
901
902 uid_swapped = 1;
634 temporarily_use_uid(pw); 903 temporarily_use_uid(pw);
635 904
636 close(p[1]);
637 if ((f = fdopen(p[0], "r")) == NULL) {
638 error("%s: fdopen: %s", __func__, strerror(errno));
639 close(p[0]);
640 /* Don't leave zombie child */
641 kill(pid, SIGTERM);
642 while (waitpid(pid, NULL, 0) == -1 && errno == EINTR)
643 ;
644 goto out;
645 }
646 ok = check_authkeys_file(f, options.authorized_keys_command, key, pw); 905 ok = check_authkeys_file(f, options.authorized_keys_command, key, pw);
647 fclose(f);
648 906
649 while (waitpid(pid, &status, 0) == -1) { 907 if (exited_cleanly(pid, "AuthorizedKeysCommand", command) != 0)
650 if (errno != EINTR) {
651 error("%s: waitpid: %s", __func__, strerror(errno));
652 goto out;
653 }
654 }
655 if (WIFSIGNALED(status)) {
656 error("AuthorizedKeysCommand %s exited on signal %d",
657 options.authorized_keys_command, WTERMSIG(status));
658 goto out;
659 } else if (WEXITSTATUS(status) != 0) {
660 error("AuthorizedKeysCommand %s returned status %d",
661 options.authorized_keys_command, WEXITSTATUS(status));
662 goto out; 908 goto out;
663 } 909
910 /* Read completed successfully */
664 found_key = ok; 911 found_key = ok;
665 out: 912 out:
666 restore_uid(); 913 if (f != NULL)
914 fclose(f);
915 signal(SIGCHLD, osigchld);
916 for (i = 0; i < ac; i++)
917 free(av[i]);
918 free(av);
919 if (uid_swapped)
920 restore_uid();
921 free(command);
922 free(username);
923 free(key_fp);
924 free(keytext);
667 return found_key; 925 return found_key;
668} 926}
669 927
diff --git a/sshd_config.5 b/sshd_config.5
index 562dad356..e40ecedef 100644
--- a/sshd_config.5
+++ b/sshd_config.5
@@ -33,8 +33,8 @@
33.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
34.\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34.\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35.\" 35.\"
36.\" $OpenBSD: sshd_config.5,v 1.200 2015/04/29 03:48:56 dtucker Exp $ 36.\" $OpenBSD: sshd_config.5,v 1.201 2015/05/21 06:38:35 djm Exp $
37.Dd $Mdocdate: April 29 2015 $ 37.Dd $Mdocdate: May 21 2015 $
38.Dt SSHD_CONFIG 5 38.Dt SSHD_CONFIG 5
39.Os 39.Os
40.Sh NAME 40.Sh NAME
@@ -234,9 +234,21 @@ The default is not to require multiple authentication; successful completion
234of a single authentication method is sufficient. 234of a single authentication method is sufficient.
235.It Cm AuthorizedKeysCommand 235.It Cm AuthorizedKeysCommand
236Specifies a program to be used to look up the user's public keys. 236Specifies a program to be used to look up the user's public keys.
237The program must be owned by root and not writable by group or others. 237The program must be owned by root, not writable by group or others and
238It will be invoked with a single argument of the username 238specified by an absolute path.
239being authenticated, and should produce on standard output zero or 239.Pp
240Arguments to
241.Cm AuthorizedKeysCommand
242may be provided using the following tokens, which will be expanded
243at runtime: %% is replaced by a literal '%', %u is replaced by the
244username being authenticated, %h is replaced by the home directory
245of the user being authenticated, %t is replaced with the key type
246offered for authentication, %f is replaced with the fingerprint of
247the key, and %k is replaced with the key being offered for authentication.
248If no arguments are specified then the username of the target user
249will be supplied.
250.Pp
251The program should produce on standard output zero or
240more lines of authorized_keys output (see AUTHORIZED_KEYS in 252more lines of authorized_keys output (see AUTHORIZED_KEYS in
241.Xr sshd 8 ) . 253.Xr sshd 8 ) .
242If a key supplied by AuthorizedKeysCommand does not successfully authenticate 254If a key supplied by AuthorizedKeysCommand does not successfully authenticate