Commit 84f30f17 authored by Jonathan Currier's avatar Jonathan Currier

Initial work on update-shell

should be fully functional
parents
ACLOCAL_AMFLAGS = -I m4
bin_PROGRAMS = update-shell update-su
update_shell_SOURCES = src/update-shell.c
update_su_SOURCES = src/update-su.c
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.69])
AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS])
AC_CONFIG_SRCDIR([src/update-shell.c])
AC_CONFIG_HEADERS([config.h])
AM_INIT_AUTOMAKE([1.11 parallel-tests foreign no-dist-gzip dist-xz color-tests subdir-objects])
AM_SILENT_RULES([yes])
AC_CONFIG_MACRO_DIRS([m4])
# Checks for programs.
AC_PROG_CC
# Checks for libraries.
# Checks for header files.
AC_CHECK_HEADERS([stdlib.h string.h unistd.h])
# Checks for typedefs, structures, and compiler characteristics.
AC_CHECK_HEADER_STDBOOL
AC_TYPE_UID_T
AC_TYPE_SIZE_T
# Checks for library functions.
AC_FUNC_GETGROUPS
AC_CONFIG_FILES([Makefile])
AC_OUTPUT
/*
* This program is a very simply update agent.
* Configure it as a user's shell. Then when that user scp's a file
* will attempt to do a system update with said file.
*
* update package must be an uncompressed tarball
* update.tar must contain only:
* update.tar.xz - an xz compressed tarball.
* update.tar.xz.sig - gpg signature. update.tar.xz will only be
* decompressed if it matches
*
* The update tarball must NOT contain any other files.
* The contents of update.tar.xz are largely undefined, however it must
* contain an executable 'update-install' in it's root directory.
* update-shell will (indirectly) run update-install as root if the tarball
* signature matches a key in it's keyring.
*/
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <stdbool.h>
#include <dirent.h>
bool validate_fsents()
{
static const char *acceptable[] = {
".",
"..",
"update.tar",
"update.tar.xz",
"update.tar.xz.sig"
};
int lsize = sizeof(acceptable)/sizeof(acceptable[0]);
int i;
int j;
DIR *curdir = opendir(".");
struct dirent *dent;
for (i = 0, dent = readdir(curdir);dent && (i < 5) ;i++, dent = readdir(curdir))
{
j = 0;
for (j = 0; strcmp(acceptable[j], dent->d_name) && j < lsize; j++);
if (j > lsize)
return false;
}
return i == lsize;
}
/*
* This program would *almost* be a good program to actually be a shell script.
* However major reason it is a 'C' program is that safe argument parsing,
* is much harder in shell.
*/
int main(int argc, char **argv)
{
static const char incoming_scp[] = "scp -t";
static const char outgoing_scp[] = "scp -f";
int exit_code = EXIT_FAILURE;
char *system_cmd;
char tempdir[] = "/tmp/update.XXXXXX";
if (argc < 2)
{
dprintf(2, "WARNING: raw shell mode unimplemented\n");
exit(EXIT_FAILURE);
}
else if (strcmp("-c", argv[1]))
{
dprintf(2, "First argument must be shell command to run\n");
exit(EXIT_FAILURE);
}
else if (memcmp("scp ", argv[2], 4))
{
dprintf(2, "scp is the only supported command\n");
dprintf(2, "cmd recieved: \"%s\"\n", argv[2]);
exit(EXIT_FAILURE);
}
if (memcmp(incoming_scp, argv[2], sizeof(outgoing_scp) - 1))
{
dprintf(2, "Only recieving updates supported (sending files is not)\n");
exit(EXIT_FAILURE);
}
if (!mkdtemp(tempdir) ||
chdir(tempdir))
{
dprintf(2, "Allocation of temporary directory has failed\n");
goto fail_tmpdir;
}
/* If we were sent an update, save it to a fixed file name.
* That will make some suff much simpler*/
if (system("scp -t ./update.tar"))
{
dprintf(2, "scp failed\n");
goto fail_tmpdir;
}
/* check fail codes */
if (system("tar -xf update.tar"))
{
dprintf(2, "ERROR: unable to parse update tar file\n");
goto fail_tmpdir;
}
/* check that exactly the right files exist */
{
struct stat tstat;
/* check that the file exist and that there's no extras,
* which would mean that the tarball only contained two files
*/
if (!validate_fsents())
{
/* This should really be some sort of function */
dprintf(2, "scp invalid tarball contents (%m)\n");
goto fail_tmpdir;
}
}
if (system("gpgv update.tar.xz.sig update.tar.xz"))
{
dprintf(2, "update failed signature test;\n");
goto fail_tmpdir;
}
/* We invoke the suid binary update-su with an argument of the temp dir.
* it will do a tiny amount of sanity checking (the update user is suppoed
* to be well trusted, and only invoked for updates) and then run the
* 'update-install' as root.
* That is safe & sane at this point because the user had access (password
* and/or ssh keys) to the update account, AND the update was signed by
* a trusted key.
*
* So if you dont want to allow downgrades, for example, you must be sure
* to never sign an update that skips downgrade checks.
* (however note that you can not really prevent downgrades since u-boot
* console access is sufficent to load any desired code, but that would
* prevent remote downgrades even by people with access to the update
* account)
*/
if ((asprintf(&system_cmd, "update-su --source %s", tempdir) > 0) &&
!system("cat update.tar.xz | unxz | tar -x") &&
!system(system_cmd))
exit_code = EXIT_SUCCESS;
/* Note: it is, and should be valid for the update-install to issue a reboot,
* reboot -f, or a kexec. Therefore we can not put any critical code here.
* which is fine since at this point it's mostly just clean up*/
free(system_cmd);
system_cmd = NULL;
fail_tmpdir:
/* it's ok if this fails */
{
int unusued __attribute((unused)) = chdir ("/");
}
/* If we could not allocate memory, then give up.
* there's really nothing that can be done, and we were just trying
* to remote a dir in a tmpfs. */
if (asprintf(&system_cmd, "rm -rf %s", tempdir) > 0)
{
int unusued __attribute((unused)) =
system(system_cmd);
free(system_cmd);
}
return exit_code;
}
/*
*
* Fairly straight forward program.
* This program allows the update user to invoke update-install inside an
* update as root.
*
* Needs to be root:update owned.
* with suid.
*
* Upon two conditions:
* 1. requires a source directory (path must follow the update-shell directory
* creation format).
* 2. checks that the invoke either is in the update group,
* or is the update user.
*
* If those two conditions are met, the program update-install inside
* of the source directory is executed as root.
*
*/
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>
#include <getopt.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#define UPDATE_USER_NAME "update"
#define UPDATE_GROUP_NAME "update"
/* This enum must *not* have gaps in it, otherwise the arg
* parsing will fail. Since a null entry marks the end of the table*/
enum {
source_arg = 0,
};
struct option long_opts[] = {
[source_arg] = { "source", required_argument, NULL, 0},
{ }
};
/*
* Scan through our save user id, group id,
* and supplementary groups. If any of the group id's
* are the update group, or if our saved uid is the update
* user, the we are authorized, if not, we are not authorized.
*/
bool validate_cred()
{
ssize_t idlist_size;
idlist_size = getgroups(0, NULL);
struct passwd *update_uid = getpwnam(UPDATE_USER_NAME);
struct group *update_gid = getgrnam(UPDATE_GROUP_NAME);
if (idlist_size >= 0 && update_gid)
{
gid_t egid = -1;
gid_t rgid = -1;
gid_t sgid = -1;
size_t i;
uid_t *gids;
/* If the group id of the update user is 0 (root) bail out, whatever
* our env is, it is not sane.*/
if (!update_gid->gr_gid)
return false;
idlist_size += 1;
gids = calloc(idlist_size, sizeof(uid_t));
if (gids &&
(getgroups(idlist_size, gids + 1) > -1) &&
!getresgid(gids + 0, &egid, &sgid))
{
for (i = 0; i < idlist_size; i++)
{
if (gids[i] == update_gid->gr_gid)
{
free(gids);
return true;
}
}
}
free(gids);
}
/* The first condition obviously checks that we got a valid passwd entry,
* For the second, if the update user is uid 0 (root) then our env is not
* sane, lets bail out.
*/
if (update_uid && update_uid->pw_uid)
{
uid_t suid = -1;
uid_t ruid = -1;
uid_t euid = -1;
getresuid(&ruid, &euid, &suid);
if (suid == update_uid->pw_uid)
return true;
}
return false;
}
int main(int argc, char ** argv)
{
if (!validate_cred())
{
dprintf(2, "unauthorized\n");
exit(EXIT_FAILURE);
}
char *source_dir = NULL;
{
int i;
int index;
while ((i = getopt_long(argc, argv, "", long_opts, &index)) >= 0)
{
if (i == '?')
{
dprintf(2, "ERROR: unkown argument\n");
exit(EXIT_FAILURE);
}
switch (index) {
case source_arg:
source_dir = optarg;
break;
default:
/* should be unreachable */
dprintf(2, "ERROR: known argument, but case not coded\n");
exit(EXIT_FAILURE);
break;
}
}
}
if (!source_dir)
{
dprintf(2, "ERROR: source not given\n");
exit(EXIT_FAILURE);
}
{
static const char template[] = "/tmp/update.XXXXXX";
static const char match[] = "/tmp/update.";
if ((strlen(source_dir) == sizeof(template) - 1) &&
!(memcmp(source_dir, match, sizeof(match) - 1)) &&
!chdir(source_dir))
{
char *args[] = {
"./update-install",
"--source",
source_dir,
NULL
};
execv(args[0], args);
}
}
return EXIT_FAILURE;
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment