Mercurial > trustbridge
view cinst/nss-installer.c @ 1394:8d27c6d226cd
Improve debug output for list verification and no installed list
author | Andre Heinecke <andre.heinecke@intevation.de> |
---|---|
date | Fri, 23 Jan 2015 15:25:50 +0100 |
parents | 12ed0b72e9f5 |
children |
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 NSS store certificate installation process * * Reads from a file given on command line or stdin a list of * instructions in the form: * * I:\<base64 DER econded certificate\> <BR> * 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. * * To get more verbose output add the --debug parameter * as the last parameter on the command line. * */ /** * @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 <secerr.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/stat.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 "/usr/lib/thunderbird/defaults", "/usr/lib/firefox/browser/defaults" #define MOZILLA_DBNAMES "cert8.db", "key3.db", "secmod.db" #define NSSSHARED ".pki/nssdb" #define NSSSHARED_GLOBAL "/etc/skel/.pki/nssdb" #define TARGET_LINUX 1 #define DIRSEP "/" #else #define MOZILLA_DEFAULTS "Mozilla Firefox\\browser\\defaults", "Mozilla Thunderbird\\defaults" #define MOZILLA_DBNAMES NULL #define CONFDIRS "Mozilla", "Thunderbird" #define NSSSHARED "" #define TARGET_LINUX NULL #define DIRSEP "\\" #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 { ERRORPRINTF("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]<BR> * IsRelative=1<BR> * Path=Example/foo.bar * * or<BR> * [Profile0]<BR> * IsRelative=0<BR> * 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 make the default nss databases readable. * * This uses the static paths definied in this code to ensure * that only the defaults are touched. * */ #ifndef WIN32 static void make_defaults_readable() { const char *confdirs[] = { MOZILLA_DEFAULTS, NULL }; const char *filenames[] = { MOZILLA_DBNAMES, NULL }; mode_t access_mask = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; for (int i=0; confdirs[i] != NULL; i++) { for (int j=0; filenames[j] != NULL; j++) { char *realpath = NULL, *path = NULL; xasprintf (&path, "%s/profile/%s", confdirs[i], filenames[j]); realpath = port_realpath(path); xfree(path); if (!realpath) { syslog_error_printf("Failed to find %s \n", realpath); continue; } if (chmod(realpath, access_mask)) { syslog_error_printf("Failed to set access_mask on file.\n"); } xfree (realpath); } } } #endif /** * @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 }; #ifdef _WIN32 char *program_files = get_program_files_folder(); if (!program_files) { ERRORPRINTF ("Failed to look up program files folder.\n"); return NULL; } #endif for (int i=0; confdirs[i] != NULL; i++) { char *realpath = NULL, *profile_dir = NULL; #ifndef _WIN32 realpath = port_realpath(confdirs[i]); #else /* As on linux we only respect the default installation directory mozilla firefox and thunderbird change their registry key with each version as the key includes the version number. It would be error prone to search the system for every instance. So we only check the default installation directories. */ xasprintf(&realpath, "%s" DIRSEP "%s", program_files, confdirs[i]); #endif if (realpath == NULL) { DEBUGPRINTF ("Did not find directory: '%s'\n", confdirs[i]); continue; } xasprintf(&profile_dir, "%s" DIRSEP "profile", realpath); xfree(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_p(profile_dir, true)) { 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; } } } #ifdef WIN32 xfree (program_files); #endif 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. */ if (!port_mkdir_p(NSSSHARED_GLOBAL, false)) { ERRORPRINTF("Failed to create nssshared skeleton directory. \n"); } else { strv_append(&alldirs, "sql:" NSSSHARED_GLOBAL, strlen("sql:" 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. * @returns 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) { ERRORPRINTF("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) { if(CERT_ChangeCertTrust(CERT_GetDefaultCertDB(), cert, trust) == SECSuccess) { log_certificate_der (pdir, dercert->data, dercert->len, true); success = true; } } /* This could have happened on either the import cert or the cert change trust. If Import Cert fails with that error the certificate has in fact been added but with random trist bits. See NSS Bug 595861. Reference code can be found in gnome evolution under smime/lib/e-cert-db.c */ if(PORT_GetError() == SEC_ERROR_TOKEN_NOT_LOGGED_IN) { if (PK11_NeedUserInit (pk11slot)) { PK11_InitPin (pk11slot, "", ""); } if (PK11_Authenticate (pk11slot, PR_TRUE, NULL) != SECSuccess) { DEBUGPRINTF("Failed to authenticate.\n"); } else if(CERT_ChangeCertTrust(CERT_GetDefaultCertDB(), cert, trust) == SECSuccess) { log_certificate_der (pdir, dercert->data, dercert->len, true); success = true; } } if (!success) { 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] stream from standard input * @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) { ERRORPRINTF("FATAL: Invalid input: %s\n", inpl); exit(ERR_MOZ_INVALID_INPUT); } } } #ifdef IS_TAG_BUILD bool g_debug = false; #else bool g_debug = true; #endif 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: if (strcmp(argv[1], "--debug") == 0) { g_debug = true; DEBUGPRINTF("Opening STDIN for input...\n"); input_stream = stdin; break; } case 3: DEBUGPRINTF("Opening %s for input...\n", argv[1]); if ((input_stream = fopen(argv[1], "r")) == NULL) { ERRORPRINTF ("FATAL: Could not open %s for reading!\n", argv[1]); exit_code = ERR_MOZ_FAILED_TO_OPEN_INPUT; goto exit; } if (argc == 3 && strcmp(argv[2], "--debug") == 0) { g_debug = true; } break; default: ERRORPRINTF("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 #ifndef WIN32 if (is_elevated()) { make_defaults_readable(); } #endif strv_free(dbdirs); } fclose(input_stream); exit: exit(exit_code); }