view cinst/mozilla.c @ 982:85c497b45488

Merged.
author Emanuel Schuetze <emanuel@intevation.de>
date Fri, 29 Aug 2014 16:08:50 +0200
parents b3695a3399de
children 1743895b39b8
line wrap: on
line source
/* Copyright (C) 2014 by Bundesamt für Sicherheit in der Informationstechnik
 * Software engineering by Intevation GmbH
 *
 * This file is Free Software under the GNU GPL (v>=2)
 * and comes with ABSOLUTELY NO WARRANTY!
 * See LICENSE.txt for details.
 */
/**
 * @file
 * @brief Mozilla installation process
 *
 * Reads from a file given on command line or stdin a list of
 * instructions in the form:
 *
 * I:<base64 DER econded certificate>
 * R:<base64 DER econded certificate>
 * ...
 *
 * With one instruction per line. the maximum size of an input
 * line is 9999 characters (including the \r\n) at the end of the line.
 *
 * Certificates marked with I: will be installed and the ones
 * marked with R: will be searched and if available removed from
 * the databases.
 *
 * This tool tries to find all NSS databases the user has
 * access to and to execute the instructions on all of them.
 *
 * If the tool is executed with a UID of 0 or with admin privileges under
 * windows it will not look into the user directories but instead try
 * to write the system wide defaults.
 *
 * If there are other processes accessing the databases the caller
 * has to ensure that those are terminated before this process is
 * executed.
 *
 * If the same certificate is marked to be installed and to be removed
 * in one call the behavior is undefined. This should be avoided and
 * may lead to errors.
 *
 * Returns 0 on success (Even when no stores where found) an error value
 * as defined in errorcodes.h otherwise.
 *
 * Success messages are written to stdout. Errors to stderr. For logging
 * purposes each installation / removal of a certificate will be reported
 * with the profile name that it modified.
 *
 */

/**
 * @brief Needs to be defined to get strnlen()
 */
#define _POSIX_C_SOURCE 200809L

/* REMOVEME: */
#include <unistd.h>

#include <cert.h>
#include <certdb.h>
#include <certt.h>
#include <dirent.h>
#include <nss.h>
#include <pk11pub.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

#define DEBUGPREFIX "MOZ-"
#include "logging.h"

#include "certhelp.h"
#include "errorcodes.h"
#include "portpath.h"
#include "strhelp.h"
#include "nss-secitemlist.h"
#include "util.h"

#ifndef _WIN32
#define CONFDIRS ".mozilla", ".thunderbird"
/* Default installation directory of ubuntu 14.4 is respected */
#define MOZILLA_DEFAULTS "/etc/thunderbird", "/etc/firefox"
#define NSSSHARED ".pki/nssdb"
#define NSSSHARED_GLOBAL "/etc/pki/nssdb"
#define TARGET_LINUX 1
#else
#define MOZILLA_DEFAULTS 0
#define CONFDIRS "Mozilla", "Thunderbird"
#define NSSSHARED ""
#define TARGET_LINUX 0
#endif

/**
 * @brief Length of string buffers used
 *
 * The maximal length of input is defined as 9999 (+ terminating \0).
 * We use it for other other input puffers besides the IPC input, too.
 * (One size fits all).
 */
#define LINEBUFLEN 10000

#ifdef _WIN32
#define STRTOK_R strtok_s
#else
#define STRTOK_R strtok_r
#endif

/**
 * @brief Global Return Code
 *
 * This will be retuned by the programm and might be set to an
 * error code on fatal errors and to and warning code on non-fatal
 * errors.  In case of mor than one warning the warning codes will be
 * ORed together.
 */
int exit_code = 0;

/**
 * @brief Return configuration base directory.
 * @returns A pointer to a string containing the path to the base
 * directory holding the configuration directories for e.g. mozilla
 * and thunderbird.
 */
static char *
get_conf_basedir()
{
  char *cdir, *envvar;

  if (TARGET_LINUX)
    envvar = "HOME" ;
  else
    envvar = "APPDATA";

  if ((cdir = getenv(envvar)) != NULL)
    return cdir;
  else
    {
      DEBUGPRINTF("FATAL!  No %s in environment.\n", envvar);
      exit(ERR_MOZ_HOMELESS);
    }
}

/**
 * @brief Get a list of all mozilla profile directories
 *
 * Parse the profiles.ini and extract all profile paths from that.
 * The expected data is in the form:
 *
 * [Profile99]
 * IsRelative=1
 * Path=Example/fooo.bar
 *
 * or
 * [Profile0]
 * IsRelative=0
 * Path=c:\foo\bar\baz
 *
 * Mozilla also accepts the ini file on Windows even if it is UTF-16
 * encoded but never writes UTF-16 on its own.  So currently we ignore
 * this special case.
 *
 * @param[in] inifile_name path of the profile.ini to read.
 * @return NULL terminated array of strings containing containing the
 * absolute path of the profile directories. The array needs to
 * be freed by the caller.
 */
static char **
get_profile_dirs (char *inifile_name)
{
  char **dirs = NULL;
  char *inifile_dirname;
  FILE *inifile;
  char line[LINEBUFLEN];
  char *key;
  char *value;
  char *path = NULL;
  char *fqpath;
  bool inprofile = false;
  bool relative_path = false;
  char *saveptr;

  if ((inifile = fopen(inifile_name, "r")) != NULL)
    {
      DEBUGPRINTF("Searching for profile paths in: '%s'\n", inifile_name);

      inifile_dirname = port_dirname(inifile_name);
      while (fgets(line, LINEBUFLEN, inifile) != NULL)
        {
          /* Determine if we are in an profile section */
          if (str_starts_with(line, "[Profile"))
            {
              relative_path = false;
              inprofile = true;
            }
          else if (line[0] == '[')
            inprofile = false;

          /* If we are in a profile parse path related stuff */
          if (inprofile)
            {
              saveptr = NULL;
              key = STRTOK_R(line, "=", &saveptr);
              value = STRTOK_R(NULL, "=", &saveptr);
              str_trim(&value);
              if (str_equal(key, "Path"))
                {
                  if (relative_path)
                    xasprintf(&path, "%s/%s", inifile_dirname, value);
                  else
                    xasprintf(&path, "%s", value);
                  if ((fqpath = port_realpath(path)) != NULL)
                    {
                      DEBUGPRINTF("Found profile path: '%s'\n", fqpath);
                      strv_append(&dirs, fqpath, strlen(fqpath));
                      free (fqpath);
                    }
                  else
                    {
                      DEBUGPRINTF("WARN!  Non existent profile path: '%s'\n", path);
                      exit_code |= WARN_MOZ_PROFILE_DOES_NOT_EXIST;
                    }
                  free(path);
                }
              else if (str_equal(key, "IsRelative") &&
                       str_starts_with(value, "1"))
                relative_path = true;
            }
        }
      fclose(inifile);
    }
  else
    {
      DEBUGPRINTF("WARN!  Could not open ini file: '%s'\n", inifile_name);
      exit_code |= WARN_MOZ_FAILED_TO_OPEN_INI;
    }
  return dirs;
}

/**
 * @brief Search for mozilla profiles.ini files
 *
 * Use well known paths and heuristics to find the current users
 * profiles.ini files on GNU/Linux and Windows systems.
 *
 * @return NULL terminated array of strings containing the absolute
 * path of the profiles.ini files.  The array needs to be freed by the
 * caller.
 */
static char **
get_profile_inis ()
{
  char **inis = NULL;
  char *mozpath, *fqpath, *subpath, *ppath;
  DIR *mozdir;
  struct dirent *mozdirent;
  char *confbase = get_conf_basedir();
  const char *confdirs[] = { CONFDIRS, NULL };

  for (int i=0; confdirs[i] != NULL; i++)
    {
      xasprintf(&mozpath,"%s/%s", confbase, confdirs[i]);
      if ((mozdir = opendir(mozpath)) != NULL)
        {
          while ((mozdirent = readdir(mozdir)) != NULL)
            {
              xasprintf(&subpath, "%s/%s/%s",
                        confbase,
                        confdirs[i],
                        mozdirent->d_name);
              if (port_isdir(subpath)
                  && (strcmp(mozdirent->d_name, "..") != 0))
                {
                  xasprintf(&ppath, "%s/%s/%s/%s",
                            confbase,
                            confdirs[i],
                            mozdirent->d_name,
                            "profiles.ini");
                  DEBUGPRINTF("checking for %s...\n", ppath);
                  if ((fqpath = port_realpath(ppath)) != NULL)
                    {
                      strv_append(&inis, fqpath, strlen(fqpath));
                      DEBUGPRINTF("Found mozilla ini file: '%s'\n", fqpath);
                      free(fqpath);
                    }
                  free(ppath);
                }
              free(subpath);
            }
          closedir(mozdir);
        }
      else
        {
          DEBUGPRINTF("Could not open %s/%s\n", confbase, confdirs[i]);
        }
      free(mozpath);
    }
  if (inis == NULL)
    {
      DEBUGPRINTF("No ini files found - will do nothing!\n");
    }
  return inis;
}

/**
 * @brief Collect the default profile directories for mozilla software
 *
 * If the default directory is found but not the profiles subdirectory
 * this will create the profiles subdirectory.
 *
 * @return NULL terminated array of strings containing the absolute path
 * to the default profile directories. Needs to be freed by the caller.
 */
static char**
get_default_profile_dirs()
{
  char **retval = NULL;

  const char *confdirs[] = { MOZILLA_DEFAULTS, NULL };

  for (int i=0; confdirs[i] != NULL; i++)
    {
      char * realpath = port_realpath(confdirs[i]);
      char * profile_dir = NULL;
      if (realpath == NULL)
        {
          DEBUGPRINTF ("Did not find directory: '%s'\n", confdirs[i]);
          continue;
        }
      xasprintf(&profile_dir, "%s/profile", realpath);
      if (port_isdir(profile_dir))
        {
          DEBUGPRINTF("Found default directory: '%s'\n", profile_dir);
          /* All is well */
          strv_append (&retval, profile_dir, strlen(profile_dir));
          xfree(profile_dir);
          profile_dir = NULL;
          continue;
        }
      else
        {
          /* Create the directory */
          if (port_fileexits(profile_dir))
            {
              DEBUGPRINTF ("Path: '%s' is not a directory but it exists. Skipping.\n",
                           profile_dir);
              xfree(profile_dir);
              profile_dir = NULL;
              continue;
            }
          else
            {
              /* Lets create it */
              if (!port_mkdir(profile_dir))
                {
                  ERRORPRINTF ("Failed to create directory: '%s'\n", profile_dir);
                  xfree(profile_dir);
                  profile_dir = NULL;
                  continue;
                }
              strv_append (&retval, profile_dir, strlen(profile_dir));
              xfree(profile_dir);
              profile_dir = NULL;
            }
        }
    }
  return retval;
}

/**
 * @brief Collect all mozilla profile directories of current user.
 * @return NULL terminated array of strings containing the absolute
 * path of the profile directories.  The array needs to be freed by the
 * caller.
 */
static char**
get_all_nssdb_dirs()
{
  char **mozinis, **pdirs;
  char **alldirs = NULL;

  if (is_elevated())
    {
#ifndef _WIN32
      /* NSS Shared db does not exist under windows. */
      strv_append(&alldirs, NSSSHARED_GLOBAL, strlen(NSSSHARED_GLOBAL));
#endif
      pdirs = get_default_profile_dirs();
      if (pdirs != NULL)
        {
          for (int i=0; pdirs[i] != NULL; i++)
            {
              strv_append(&alldirs, pdirs[i], strlen(pdirs[i]));
            }
          strv_free(pdirs);
        }
      return alldirs;
    }
  /* Search Mozilla/Firefox/Thunderbird profiles */
  if ((mozinis = get_profile_inis()) != NULL)
    {
      for (int i=0; mozinis[i] != NULL; i++)
        {
          pdirs =
            get_profile_dirs(mozinis[i]);
          if (pdirs != NULL)
            {
              for (int i=0; pdirs[i] != NULL; i++)
                {
                  strv_append(&alldirs, pdirs[i], strlen(pdirs[i]));
                }
              strv_free(pdirs);
            }
        }
      strv_free(mozinis);
    }
  /* Search for NSS shared DB (used by Chrome/Chromium on GNU/Linux) */
  if (TARGET_LINUX)
    {
      char *path, *fqpath, *sqlpath;
      xasprintf(&path, "%s/%s", get_conf_basedir(), NSSSHARED);
      if ((fqpath = port_realpath(path)) != NULL)
        {
          xasprintf(&sqlpath, "sql:%s", fqpath);
          strv_append(&alldirs, sqlpath, strlen(sqlpath));
          free(sqlpath);
          free(fqpath);
        }
      free(path);
    }
  return alldirs;
}

#ifdef DEBUGOUTPUT
/**
 * @brief list certificates from nss certificate store
 * @param[in] confdir the directory with the certificate store
 */
static void
DEBUG_nss_list_certs (char *confdir)
{
  CERTCertList *list;
  CERTCertListNode *node;
  char *name;

  if (NSS_Initialize(confdir, "", "", "secmod.db", NSS_INIT_READONLY)
      == SECSuccess)
    {
      DEBUGPRINTF("Listing certs in \"%s\"\n", confdir);
      list = PK11_ListCerts(PK11CertListAll, NULL);
      for (node = CERT_LIST_HEAD(list); !CERT_LIST_END(node, list);
           node = CERT_LIST_NEXT(node))
        {
          name = node->appData;

          DEBUGPRINTF("Found certificate \"%s\"\n", name);
        }
      /* According to valgrind this leaks memory in the list.
         We could not find API documentation to better free this
         so we accept the leakage here in case of debug. */
      CERT_DestroyCertList(list);
      NSS_Shutdown();
    }
  else
    {
      DEBUGPRINTF("Could not open nss certificate store in %s!\n", confdir);
    }
}
#endif

/**
 * @brief Create a string with the name for cert in SECItem.
 *
 * Should be freed by caller.
 * @param[in] secitemp ponts to an SECItem holding the DER certificate.
 * @retruns a string of the from "CN of Subject - O of Subject"
 */
static char *
nss_cert_name(SECItem *secitemp)
{
  char *cn_str, *o_str, *name;
  size_t name_len;
  cn_str = x509_parse_subject(secitemp->data, secitemp->len, CERT_OID_CN);
  o_str = x509_parse_subject(secitemp->data, secitemp->len, CERT_OID_O);
  if (!cn_str || !o_str)
    {
      DEBUGPRINTF("FATAL: Could not parse certificate!");
      exit(ERR_INVALID_CERT);
    }
  name_len = strlen(cn_str) + strlen(o_str) + 4;
  name = (char *)xmalloc(name_len);
  snprintf(name, name_len, "%s - %s", cn_str, o_str);
  free(cn_str);
  free(o_str);
  return name;
}

/**
 * @brief Convert a base64 encoded DER certificate to SECItem
 * @param[in] b64 pointer to the base64 encoded certificate
 * @param[in] b64len length of the base64 encoded certificate
 * @param[out] secitem pointer to the SECItem in which to store the
 * raw DER certifiacte.
 * @returns true on success and false on failure
 */
static bool
base64_to_secitem(char *b64, size_t b64len, SECItem *secitem)
{
  unsigned char *dercert = NULL;
  size_t dercertlen;

  if ((str_base64_decode((char **)(&dercert), &dercertlen,
                         b64, b64len) == 0) &&
      (dercertlen > 0))
    {
      secitem->data = dercert;
      secitem->len = (unsigned int) dercertlen;
      return true;
    }
  else
    {
      DEBUGPRINTF("Base64 decode failed for: %s\n", b64);
    }
  return false;
}

/**
 * @brief Store DER certificate in mozilla store.
 * @param[in] pdir the mozilla profile directory with the certificate
 * store to manipulate.
 * @param[in] dercert pointer to a SECItem holding the DER certificate
 * to install
 * @returns true on success and false on failure
 */
static bool
import_cert(char *pdir, SECItem *dercert)
{
  PK11SlotInfo *pk11slot = NULL;
  CERTCertTrust *trust = NULL;
  CERTCertificate *cert = NULL;
  bool success = false;
  char *cert_name = nss_cert_name(dercert);

  DEBUGPRINTF("INSTALLING cert: '%s' to: %s\n", cert_name, pdir);
  pk11slot = PK11_GetInternalKeySlot();
  cert = CERT_DecodeCertFromPackage((char *)dercert->data,
                                    (int)dercert->len);
  trust = (CERTCertTrust *)xmalloc(sizeof(CERTCertTrust));
  CERT_DecodeTrustString(trust, "C,C,C");
  if ((PK11_ImportCert(pk11slot, cert, CK_INVALID_HANDLE,
                       cert_name, PR_FALSE)
       == SECSuccess) &&
      (CERT_ChangeCertTrust(CERT_GetDefaultCertDB(), cert, trust)
       == SECSuccess))
    {
      log_certificate_der (pdir, dercert->data, dercert->len, true);
      success = true;
    }
  else
    {
      DEBUGPRINTF("Failed to install certificate '%s' to '%s'!\n", cert_name, pdir);
      ERRORPRINTF("Error installing certificate err: %i\n", PORT_GetError());
    }
  CERT_DestroyCertificate (cert);
  free(trust);
  PK11_FreeSlot(pk11slot);

  free(cert_name);
  return success;
}

/**
 * @brief Remove DER certificate from mozilla store.
 * @param[in] pdir the mozilla profile directory with the certificate
 * store to manipulate.
 * @param[in] dercert pointer to a SECItem holding the DER certificate
 * to remove
 * @returns true on success and false on failure
 */
static bool
remove_cert(char *pdir, SECItem *dercert)
{
  PK11SlotInfo *pk11slot = NULL;
  bool success = false;
  char *cert_name = nss_cert_name(dercert);
  CERTCertificate *cert = NULL;

  DEBUGPRINTF("REMOVING cert: '%s' from: %s\n", cert_name, pdir);
  if (NSS_Initialize(pdir, "", "", "secmod.db", 0) == SECSuccess)
    {
      pk11slot = PK11_GetInternalKeySlot();
      cert = PK11_FindCertFromDERCertItem(pk11slot,
                                          dercert, NULL);
      if (cert != NULL)
        {
          if (SEC_DeletePermCertificate(cert) == SECSuccess)
            {
              success = true;
              log_certificate_der (pdir, dercert->data, dercert->len, false);
            }
          else
            {
              DEBUGPRINTF("Failed to remove certificate '%s' from '%s'!\n", cert_name, pdir);
            }
          CERT_DestroyCertificate(cert);
        }
      else
        {
          DEBUGPRINTF("Could not find Certificate '%s' in store '%s'.\n", cert_name, pdir);
        }
      PK11_FreeSlot(pk11slot);
      NSS_Shutdown();
    }
  else
    {
      DEBUGPRINTF("Could not open nss certificate store in %s!\n", pdir);
    }
  free(cert_name);
  return success;
}

/**
 * @brief Apply a function to a list of certificates and profiles
 *
 * The function must have the signature:
 *
 *   bool function(char *pdir, SECItem der_cert)
 *
 * where pdir is the path of an profile and der_cert is an raw DER
 * formatted certificate.  The function must return true on success
 * and false on failure.
 *
 * This function is intended for use with the import_cert and
 * remove_cert functions.
 *
 * @param[in] fn the function to apply
 * @param[inout] certs a secitem list holding the certificates
 * the list will be change (emptied)!
 * @param[in] pdirs the NULL terminated list of profile directories
 * @returns true on success and false on failure
 */
bool
apply_to_certs_and_profiles(bool fn(char *, SECItem *),
                            seciteml_t **certs, char **pdirs)
{
  bool success = true;

  for (int i=0; pdirs[i] != NULL; i++)
    {
      seciteml_t *iter = *certs;
      if (NSS_Initialize(pdirs[i], "", "", "secmod.db", 0) != SECSuccess)
        {
          DEBUGPRINTF("Could not open nss certificate store in %s!\n", pdirs[i]);
          continue;
        }

      while (iter != NULL && iter->item != NULL)
        {
          SECItem *cert = iter->item;
          if (! (*fn)(pdirs[i], cert))
            success = false;
          iter = iter->next;
        }
      NSS_Shutdown();
    }

  seciteml_free(certs);

  return success;
}

/**
 * @brief Parse IPC commands from standard input.
 *
 * Reads command lines (R: and I:) from standard input and puts the
 * certificates to process in two SECItem lists holding the
 * certificates in DER format.
 * @param[inout] install_list list of SECItems with certifiactes to install
 * @param[inout] remove_list list of SECItems with certifiactes to remove
 */
static void
parse_commands (FILE *stream,
                seciteml_t **install_list, seciteml_t **remove_list)
{
  char inpl[LINEBUFLEN];
  size_t inpllen;
  bool parserr = true;
  SECItem secitem;

  while ( fgets(inpl, LINEBUFLEN, stream) != NULL )
    {
      inpllen = strnlen(inpl, LINEBUFLEN);
      /* Validate input line:
       * - must be (much) longer than 3 characters
       * - must start with "*:"
       */
      if ((inpllen > 3) && (inpl[1] == ':'))
        /* Now parse Input */
        switch(inpl[0])
          {
          case 'R':
            parserr = true;
            DEBUGPRINTF("Request to remove certificate: %s\n", &inpl[2]);
            if (base64_to_secitem(&inpl[2], inpllen - 2, &secitem))
              {
                seciteml_push(remove_list, &secitem);
                parserr = false;
              }
            break;
          case 'I':
            parserr = true;
            DEBUGPRINTF("Request to install certificate: %s\n", &inpl[2]);
            if (base64_to_secitem(&inpl[2], inpllen - 2, &secitem))
              {
                seciteml_push(install_list, &secitem);
                parserr = false;
              }
            break;
          default:
            parserr = true;
          }
      else
        {
          parserr = true;
        }

      if (parserr)
        {
          DEBUGPRINTF("FATAL: Invalid input: %s\n", inpl);
          exit(ERR_MOZ_INVALID_INPUT);
        }
    }
}


int
main (int argc, char **argv)
{
  char **dbdirs;
  seciteml_t *certs_to_remove = NULL;
  seciteml_t *certs_to_add = NULL;
  FILE *input_stream;

  switch (argc)
    {
    case 1:
      DEBUGPRINTF("Opening STDIN for input...\n");
      input_stream = stdin;
      break;
    case 2:
      DEBUGPRINTF("Opening %s for input...\n", argv[1]);
      if ((input_stream = fopen(argv[1], "r")) == NULL)
        {
          DEBUGPRINTF("FATAL: Could not open %s for reading!\n",
                      argv[1]);
          exit_code = ERR_MOZ_FAILED_TO_OPEN_INPUT;
          goto exit;
        }
      break;
    default:
      DEBUGPRINTF("FATAL: Wrong number of arguments!\n");
      exit_code = ERR_MOZ_WRONG_ARGC;
      goto exit;
    }

  dbdirs =
    get_all_nssdb_dirs();

  if (dbdirs != NULL)
    {
      parse_commands(input_stream, &certs_to_add, &certs_to_remove);

#ifdef DEBUGOUTPUT
      DEBUGPRINTF("OLD List of installed certs:\n");
      for (int i=0; dbdirs[i] != NULL; i++)
        DEBUG_nss_list_certs(dbdirs[i]);
#endif

      if (! apply_to_certs_and_profiles(remove_cert, &certs_to_remove, dbdirs))
        exit_code |= WARN_MOZ_COULD_NOT_REMOVE_CERT;

      if (! apply_to_certs_and_profiles(import_cert, &certs_to_add, dbdirs))
        exit_code |= WARN_MOZ_COULD_NOT_ADD_CERT;

#ifdef DEBUGOUTPUT
      DEBUGPRINTF("NEW List of installed certs:\n");
      for (int i=0; dbdirs[i] != NULL; i++)
        DEBUG_nss_list_certs(dbdirs[i]);
#endif

      strv_free(dbdirs);
    }

  fclose(input_stream);

exit:
  exit(exit_code);
}

http://wald.intevation.org/projects/trustbridge/