view cinst/mozilla.c @ 831:747a48996c1f

(Issue13) Precompile uninstaller Create-dist-packge now creates a temporary installer that only writes the uninstaller. Then it excutes this installer (using wine) to create the uninstaller. That uninstaller is then packaged normaly and packaged instead of the written uninstaller.
author Andre Heinecke <andre.heinecke@intevation.de>
date Thu, 24 Jul 2014 15:59:00 +0200
parents 2303caf56dbb
children 698b6a9bd75e
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 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 eb 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"

#ifndef _WIN32
#define CONFDIRS ".mozilla", ".thunderbird"
#define NSSSHARED ".pki/nssdb"
#define TARGET_LINUX 1
#else
#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

/**
 * @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;

  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)
            {
              key = strtok(line, "=");
              value = strtok(NULL, "=");
              str_trim(&value);
              if (str_equal(key, "Path"))
                {
                  if (relative_path)
                    xasprintf(&path, "%s/%s", inifile_dirname, value);
                  else
                    xasprintf(&path, "%s", value); /* FIXME: LOOKS STUPID! */
                  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 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;
  /* 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);
      }
      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/