diff --git a/agent/genkey.c b/agent/genkey.c index 7660443ca..563407253 100644 --- a/agent/genkey.c +++ b/agent/genkey.c @@ -1,633 +1,641 @@ /* genkey.c - Generate a keypair * Copyright (C) 2002, 2003, 2004, 2007, 2010 Free Software Foundation, Inc. * Copyright (C) 2015 g10 Code GmbH. * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include #include #include #include #include #include #include "agent.h" #include "../common/i18n.h" #include "../common/exechelp.h" #include "../common/sysutils.h" static int store_key (gcry_sexp_t private, const char *passphrase, int force, unsigned long s2k_count, time_t timestamp) { int rc; unsigned char *buf; size_t len; unsigned char grip[20]; if ( !gcry_pk_get_keygrip (private, grip) ) { log_error ("can't calculate keygrip\n"); return gpg_error (GPG_ERR_GENERAL); } len = gcry_sexp_sprint (private, GCRYSEXP_FMT_CANON, NULL, 0); log_assert (len); buf = gcry_malloc_secure (len); if (!buf) return out_of_core (); len = gcry_sexp_sprint (private, GCRYSEXP_FMT_CANON, buf, len); log_assert (len); if (passphrase) { unsigned char *p; rc = agent_protect (buf, passphrase, &p, &len, s2k_count); if (rc) { xfree (buf); return rc; } xfree (buf); buf = p; } rc = agent_write_private_key (grip, buf, len, force, NULL, NULL, timestamp); xfree (buf); return rc; } /* Count the number of non-alpha characters in S. Control characters and non-ascii characters are not considered. */ static size_t nonalpha_count (const char *s) { size_t n; for (n=0; *s; s++) if (isascii (*s) && ( isdigit (*s) || ispunct (*s) )) n++; return n; } /* Check PW against a list of pattern. Return 0 if PW does not match these pattern. If CHECK_CONSTRAINTS_NEW_SYMKEY is set in flags and --check-sym-passphrase-pattern has been configured, use the pattern file from that option. */ static int do_check_passphrase_pattern (ctrl_t ctrl, const char *pw, unsigned int flags) { gpg_error_t err = 0; const char *pgmname = gnupg_module_name (GNUPG_MODULE_NAME_CHECK_PATTERN); estream_t stream_to_check_pattern = NULL; const char *argv[10]; - pid_t pid; + gnupg_process_t proc; int result, i; const char *pattern; char *patternfname; (void)ctrl; pattern = opt.check_passphrase_pattern; if ((flags & CHECK_CONSTRAINTS_NEW_SYMKEY) && opt.check_sym_passphrase_pattern) pattern = opt.check_sym_passphrase_pattern; if (!pattern) return 1; /* Oops - Assume password should not be used */ if (strchr (pattern, '/') || strchr (pattern, '\\') || (*pattern == '~' && pattern[1] == '/')) patternfname = make_absfilename_try (pattern, NULL); else patternfname = make_filename_try (gnupg_sysconfdir (), pattern, NULL); if (!patternfname) { log_error ("error making filename from '%s': %s\n", pattern, gpg_strerror (gpg_error_from_syserror ())); return 1; /* Do not pass the check. */ } /* Make debugging a broken config easier by printing a useful error * message. */ if (gnupg_access (patternfname, F_OK)) { log_error ("error accessing '%s': %s\n", patternfname, gpg_strerror (gpg_error_from_syserror ())); xfree (patternfname); return 1; /* Do not pass the check. */ } i = 0; argv[i++] = "--null"; argv[i++] = "--", argv[i++] = patternfname, argv[i] = NULL; log_assert (i < sizeof argv); - if (gnupg_spawn_process (pgmname, argv, NULL, 0, - &stream_to_check_pattern, NULL, NULL, &pid)) + if (gnupg_process_spawn (pgmname, argv, + GNUPG_PROCESS_STDIN_PIPE, + NULL, NULL, &proc)) result = 1; /* Execute error - assume password should no be used. */ else { + int status; + + gnupg_process_get_streams (proc, 0, &stream_to_check_pattern, + NULL, NULL); + es_set_binary (stream_to_check_pattern); if (es_fwrite (pw, strlen (pw), 1, stream_to_check_pattern) != 1) { err = gpg_error_from_syserror (); log_error (_("error writing to pipe: %s\n"), gpg_strerror (err)); result = 1; /* Error - assume password should not be used. */ } else es_fflush (stream_to_check_pattern); es_fclose (stream_to_check_pattern); - if (gnupg_wait_process (pgmname, pid, 1, NULL)) + gnupg_process_wait (proc, 1); + gnupg_process_ctl (proc, GNUPG_PROCESS_GET_EXIT_ID, &status); + if (status) result = 1; /* Helper returned an error - probably a match. */ else result = 0; /* Success; i.e. no match. */ - gnupg_release_process (pid); + gnupg_process_release (proc); } xfree (patternfname); return result; } static int take_this_one_anyway (ctrl_t ctrl, const char *desc, const char *anyway_btn) { return agent_get_confirmation (ctrl, desc, anyway_btn, L_("Enter new passphrase"), 0); } /* Check whether the passphrase PW is suitable. Returns 0 if the * passphrase is suitable and true if it is not and the user should be * asked to provide a different one. If FAILED_CONSTRAINT is set, a * message describing the problem is returned at FAILED_CONSTRAINT. * The FLAGS are: * CHECK_CONSTRAINTS_NOT_EMPTY * Do not allow an empty passphrase * CHECK_CONSTRAINTS_NEW_SYMKEY * Hint that the passphrase is used for a new symmetric key. */ int check_passphrase_constraints (ctrl_t ctrl, const char *pw, unsigned int flags, char **failed_constraint) { gpg_error_t err = 0; unsigned int minlen = opt.min_passphrase_len; unsigned int minnonalpha = opt.min_passphrase_nonalpha; char *msg1 = NULL; char *msg2 = NULL; char *msg3 = NULL; int no_empty = !!(flags & CHECK_CONSTRAINTS_NOT_EMPTY); if (ctrl && ctrl->pinentry_mode == PINENTRY_MODE_LOOPBACK) return 0; if (!pw) pw = ""; /* The first check is to warn about an empty passphrase. */ if (!*pw) { const char *desc = (opt.enforce_passphrase_constraints || no_empty? L_("You have not entered a passphrase!%0A" "An empty passphrase is not allowed.") : L_("You have not entered a passphrase - " "this is in general a bad idea!%0A" "Please confirm that you do not want to " "have any protection on your key.")); err = 1; if (failed_constraint) { if (opt.enforce_passphrase_constraints || no_empty) *failed_constraint = xstrdup (desc); else err = take_this_one_anyway (ctrl, desc, L_("Yes, protection is not needed")); } goto leave; } /* Now check the constraints and collect the error messages unless in silent mode which returns immediately. */ if (utf8_charcount (pw, -1) < minlen ) { if (!failed_constraint) { err = gpg_error (GPG_ERR_INV_PASSPHRASE); goto leave; } msg1 = xtryasprintf ( ngettext ("A passphrase should be at least %u character long.", "A passphrase should be at least %u characters long.", minlen), minlen ); if (!msg1) { err = gpg_error_from_syserror (); goto leave; } } if (nonalpha_count (pw) < minnonalpha ) { if (!failed_constraint) { err = gpg_error (GPG_ERR_INV_PASSPHRASE); goto leave; } msg2 = xtryasprintf ( ngettext ("A passphrase should contain at least %u digit or%%0A" "special character.", "A passphrase should contain at least %u digits or%%0A" "special characters.", minnonalpha), minnonalpha ); if (!msg2) { err = gpg_error_from_syserror (); goto leave; } } /* If configured check the passphrase against a list of known words and pattern. The actual test is done by an external program. The warning message is generic to give the user no hint on how to circumvent this list. */ if (*pw && (opt.check_passphrase_pattern || opt.check_sym_passphrase_pattern) && do_check_passphrase_pattern (ctrl, pw, flags)) { if (!failed_constraint) { err = gpg_error (GPG_ERR_INV_PASSPHRASE); goto leave; } msg3 = xtryasprintf (L_("A passphrase may not be a known term or match%%0A" "certain pattern.")); if (!msg3) { err = gpg_error_from_syserror (); goto leave; } } if (failed_constraint && (msg1 || msg2 || msg3)) { char *msg; size_t n; msg = strconcat (L_("Warning: You have entered an insecure passphrase."), "%0A%0A", msg1? msg1 : "", msg1? "%0A" : "", msg2? msg2 : "", msg2? "%0A" : "", msg3? msg3 : "", msg3? "%0A" : "", NULL); if (!msg) { err = gpg_error_from_syserror (); goto leave; } /* Strip a trailing "%0A". */ n = strlen (msg); if (n > 3 && !strcmp (msg + n - 3, "%0A")) msg[n-3] = 0; err = 1; if (opt.enforce_passphrase_constraints) *failed_constraint = msg; else { err = take_this_one_anyway (ctrl, msg, L_("Take this one anyway")); xfree (msg); } } leave: xfree (msg1); xfree (msg2); xfree (msg3); return err; } /* Callback function to compare the first entered PIN with the one currently being entered. */ static gpg_error_t reenter_compare_cb (struct pin_entry_info_s *pi) { const char *pin1 = pi->check_cb_arg; if (!strcmp (pin1, pi->pin)) return 0; /* okay */ return gpg_error (GPG_ERR_BAD_PASSPHRASE); } /* Ask the user for a new passphrase using PROMPT. On success the function returns 0 and store the passphrase at R_PASSPHRASE; if the user opted not to use a passphrase NULL will be stored there. The user needs to free the returned string. In case of an error and error code is returned and NULL stored at R_PASSPHRASE. */ gpg_error_t agent_ask_new_passphrase (ctrl_t ctrl, const char *prompt, char **r_passphrase) { gpg_error_t err; const char *text1 = prompt; const char *text2 = L_("Please re-enter this passphrase"); char *initial_errtext = NULL; struct pin_entry_info_s *pi, *pi2; *r_passphrase = NULL; if (ctrl->pinentry_mode == PINENTRY_MODE_LOOPBACK) { size_t size; unsigned char *buffer; err = pinentry_loopback (ctrl, "NEW_PASSPHRASE", &buffer, &size, MAX_PASSPHRASE_LEN); if (!err) { if (size) { buffer[size] = 0; *r_passphrase = buffer; } else *r_passphrase = NULL; } return err; } pi = gcry_calloc_secure (1, sizeof (*pi) + MAX_PASSPHRASE_LEN + 1); if (!pi) return gpg_error_from_syserror (); pi2 = gcry_calloc_secure (1, sizeof (*pi2) + MAX_PASSPHRASE_LEN + 1); if (!pi2) { err = gpg_error_from_syserror (); xfree (pi); return err; } pi->max_length = MAX_PASSPHRASE_LEN + 1; pi->max_tries = 3; pi->with_qualitybar = 0; pi->with_repeat = 1; pi2->max_length = MAX_PASSPHRASE_LEN + 1; pi2->max_tries = 3; pi2->check_cb = reenter_compare_cb; pi2->check_cb_arg = pi->pin; next_try: err = agent_askpin (ctrl, text1, NULL, initial_errtext, pi, NULL, 0); xfree (initial_errtext); initial_errtext = NULL; if (!err) { if (check_passphrase_constraints (ctrl, pi->pin, 0, &initial_errtext)) { pi->failed_tries = 0; pi2->failed_tries = 0; goto next_try; } /* Unless the passphrase is empty or the pinentry told us that it already did the repetition check, ask to confirm it. */ if (*pi->pin && !pi->repeat_okay) { err = agent_askpin (ctrl, text2, NULL, NULL, pi2, NULL, 0); if (gpg_err_code (err) == GPG_ERR_BAD_PASSPHRASE) { /* The re-entered one did not match and the user did not hit cancel. */ initial_errtext = xtrystrdup (L_("does not match - try again")); if (initial_errtext) goto next_try; err = gpg_error_from_syserror (); } } } if (!err && *pi->pin) { /* User wants a passphrase. */ *r_passphrase = xtrystrdup (pi->pin); if (!*r_passphrase) err = gpg_error_from_syserror (); } xfree (initial_errtext); xfree (pi2); xfree (pi); return err; } /* Generate a new keypair according to the parameters given in KEYPARAM. If CACHE_NONCE is given first try to lookup a passphrase using the cache nonce. If NO_PROTECTION is true the key will not be protected by a passphrase. If OVERRIDE_PASSPHRASE is true that passphrase will be used for the new key. If TIMESTAMP is not zero it will be recorded as creation date of the key (unless extended format is disabled) . */ int agent_genkey (ctrl_t ctrl, const char *cache_nonce, time_t timestamp, const char *keyparam, size_t keyparamlen, int no_protection, const char *override_passphrase, int preset, membuf_t *outbuf) { gcry_sexp_t s_keyparam, s_key, s_private, s_public; char *passphrase_buffer = NULL; const char *passphrase; int rc; size_t len; char *buf; rc = gcry_sexp_sscan (&s_keyparam, NULL, keyparam, keyparamlen); if (rc) { log_error ("failed to convert keyparam: %s\n", gpg_strerror (rc)); return gpg_error (GPG_ERR_INV_DATA); } /* Get the passphrase now, cause key generation may take a while. */ if (override_passphrase) passphrase = override_passphrase; else if (no_protection || !cache_nonce) passphrase = NULL; else { passphrase_buffer = agent_get_cache (ctrl, cache_nonce, CACHE_MODE_NONCE); passphrase = passphrase_buffer; } if (passphrase || no_protection) ; else { rc = agent_ask_new_passphrase (ctrl, L_("Please enter the passphrase to%0A" "protect your new key"), &passphrase_buffer); if (rc) { gcry_sexp_release (s_keyparam); return rc; } passphrase = passphrase_buffer; } rc = gcry_pk_genkey (&s_key, s_keyparam ); gcry_sexp_release (s_keyparam); if (rc) { log_error ("key generation failed: %s\n", gpg_strerror (rc)); xfree (passphrase_buffer); return rc; } /* break out the parts */ s_private = gcry_sexp_find_token (s_key, "private-key", 0); if (!s_private) { log_error ("key generation failed: invalid return value\n"); gcry_sexp_release (s_key); xfree (passphrase_buffer); return gpg_error (GPG_ERR_INV_DATA); } s_public = gcry_sexp_find_token (s_key, "public-key", 0); if (!s_public) { log_error ("key generation failed: invalid return value\n"); gcry_sexp_release (s_private); gcry_sexp_release (s_key); xfree (passphrase_buffer); return gpg_error (GPG_ERR_INV_DATA); } gcry_sexp_release (s_key); s_key = NULL; /* store the secret key */ if (DBG_CRYPTO) log_debug ("storing private key\n"); rc = store_key (s_private, passphrase, 0, ctrl->s2k_count, timestamp); if (!rc) { if (!cache_nonce) { char tmpbuf[12]; gcry_create_nonce (tmpbuf, 12); cache_nonce = bin2hex (tmpbuf, 12, NULL); } if (cache_nonce && !no_protection && !agent_put_cache (ctrl, cache_nonce, CACHE_MODE_NONCE, passphrase, ctrl->cache_ttl_opt_preset)) agent_write_status (ctrl, "CACHE_NONCE", cache_nonce, NULL); if (preset && !no_protection) { unsigned char grip[20]; char hexgrip[40+1]; if (gcry_pk_get_keygrip (s_private, grip)) { bin2hex(grip, 20, hexgrip); rc = agent_put_cache (ctrl, hexgrip, CACHE_MODE_ANY, passphrase, ctrl->cache_ttl_opt_preset); } } } xfree (passphrase_buffer); passphrase_buffer = NULL; passphrase = NULL; gcry_sexp_release (s_private); if (rc) { gcry_sexp_release (s_public); return rc; } /* return the public key */ if (DBG_CRYPTO) log_debug ("returning public key\n"); len = gcry_sexp_sprint (s_public, GCRYSEXP_FMT_CANON, NULL, 0); log_assert (len); buf = xtrymalloc (len); if (!buf) { gpg_error_t tmperr = out_of_core (); gcry_sexp_release (s_private); gcry_sexp_release (s_public); return tmperr; } len = gcry_sexp_sprint (s_public, GCRYSEXP_FMT_CANON, buf, len); log_assert (len); put_membuf (outbuf, buf, len); gcry_sexp_release (s_public); xfree (buf); return 0; } /* Apply a new passphrase to the key S_SKEY and store it. If PASSPHRASE_ADDR and *PASSPHRASE_ADDR are not NULL, use that passphrase. If PASSPHRASE_ADDR is not NULL store a newly entered passphrase at that address. */ gpg_error_t agent_protect_and_store (ctrl_t ctrl, gcry_sexp_t s_skey, char **passphrase_addr) { gpg_error_t err; if (passphrase_addr && *passphrase_addr) { /* Take an empty string as request not to protect the key. */ err = store_key (s_skey, **passphrase_addr? *passphrase_addr:NULL, 1, ctrl->s2k_count, 0); } else { char *pass = NULL; if (passphrase_addr) { xfree (*passphrase_addr); *passphrase_addr = NULL; } err = agent_ask_new_passphrase (ctrl, L_("Please enter the new passphrase"), &pass); if (!err) err = store_key (s_skey, pass, 1, ctrl->s2k_count, 0); if (!err && passphrase_addr) *passphrase_addr = pass; else xfree (pass); } return err; } diff --git a/common/asshelp.c b/common/asshelp.c index eb3e41bf5..8b3df2a4b 100644 --- a/common/asshelp.c +++ b/common/asshelp.c @@ -1,747 +1,743 @@ /* asshelp.c - Helper functions for Assuan * Copyright (C) 2002, 2004, 2007, 2009, 2010 Free Software Foundation, Inc. * * This file is part of GnuPG. * * This file is free software; you can redistribute it and/or modify * it under the terms of either * * - the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at * your option) any later version. * * or * * - the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at * your option) any later version. * * or both in parallel, as here. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include #include #include #include #include #include #ifdef HAVE_LOCALE_H #include #endif #include "i18n.h" #include "util.h" #include "exechelp.h" #include "sysutils.h" #include "status.h" #include "membuf.h" #include "asshelp.h" /* The type we use for lock_agent_spawning. */ #ifdef HAVE_W32_SYSTEM # define lock_spawn_t HANDLE #else # define lock_spawn_t dotlock_t #endif /* The time we wait until the agent or the dirmngr are ready for operation after we started them before giving up. */ #define SECS_TO_WAIT_FOR_AGENT 5 #define SECS_TO_WAIT_FOR_KEYBOXD 5 #define SECS_TO_WAIT_FOR_DIRMNGR 5 /* A bitfield that specifies the assuan categories to log. This is identical to the default log handler of libassuan. We need to do it ourselves because we use a custom log handler and want to use the same assuan variables to select the categories to log. */ static int log_cats; #define TEST_LOG_CAT(x) (!! (log_cats & (1 << (x - 1)))) /* The assuan log monitor used to temporary inhibit log messages from * assuan. */ static int (*my_log_monitor) (assuan_context_t ctx, unsigned int cat, const char *msg); static int my_libassuan_log_handler (assuan_context_t ctx, void *hook, unsigned int cat, const char *msg) { unsigned int dbgval; if (! TEST_LOG_CAT (cat)) return 0; dbgval = hook? *(unsigned int*)hook : 0; if (!(dbgval & 1024)) return 0; /* Assuan debugging is not enabled. */ if (ctx && my_log_monitor && !my_log_monitor (ctx, cat, msg)) return 0; /* Temporary disabled. */ if (msg) log_string (GPGRT_LOGLVL_DEBUG, msg); return 1; } /* Setup libassuan to use our own logging functions. Should be used early at startup. */ void setup_libassuan_logging (unsigned int *debug_var_address, int (*log_monitor)(assuan_context_t ctx, unsigned int cat, const char *msg)) { char *flagstr; flagstr = getenv ("ASSUAN_DEBUG"); if (flagstr) log_cats = atoi (flagstr); else /* Default to log the control channel. */ log_cats = (1 << (ASSUAN_LOG_CONTROL - 1)); my_log_monitor = log_monitor; assuan_set_log_cb (my_libassuan_log_handler, debug_var_address); } /* Change the Libassuan log categories to those given by NEWCATS. NEWCATS is 0 the default category of ASSUAN_LOG_CONTROL is selected. Note, that setup_libassuan_logging overrides the values given here. */ void set_libassuan_log_cats (unsigned int newcats) { if (newcats) log_cats = newcats; else /* Default to log the control channel. */ log_cats = (1 << (ASSUAN_LOG_CONTROL - 1)); } static gpg_error_t send_one_option (assuan_context_t ctx, gpg_err_source_t errsource, const char *name, const char *value, int use_putenv) { gpg_error_t err; char *optstr; (void)errsource; if (!value || !*value) err = 0; /* Avoid sending empty strings. */ else if (asprintf (&optstr, "OPTION %s%s=%s", use_putenv? "putenv=":"", name, value) < 0) err = gpg_error_from_syserror (); else { err = assuan_transact (ctx, optstr, NULL, NULL, NULL, NULL, NULL, NULL); xfree (optstr); } return err; } /* Send the assuan commands pertaining to the pinentry environment. The OPT_* arguments are optional and may be used to override the defaults taken from the current locale. */ gpg_error_t send_pinentry_environment (assuan_context_t ctx, gpg_err_source_t errsource, const char *opt_lc_ctype, const char *opt_lc_messages, session_env_t session_env) { gpg_error_t err = 0; #if defined(HAVE_SETLOCALE) char *old_lc = NULL; #endif char *dft_lc = NULL; const char *dft_ttyname; int iterator; const char *name, *assname, *value; int is_default; iterator = 0; while ((name = session_env_list_stdenvnames (&iterator, &assname))) { value = session_env_getenv_or_default (session_env, name, NULL); if (!value) continue; if (assname) err = send_one_option (ctx, errsource, assname, value, 0); else { err = send_one_option (ctx, errsource, name, value, 1); if (gpg_err_code (err) == GPG_ERR_UNKNOWN_OPTION) err = 0; /* Server too old; can't pass the new envvars. */ } if (err) return err; } dft_ttyname = session_env_getenv_or_default (session_env, "GPG_TTY", &is_default); if (dft_ttyname && !is_default) dft_ttyname = NULL; /* We need the default value. */ /* Send the value for LC_CTYPE. */ #if defined(HAVE_SETLOCALE) && defined(LC_CTYPE) old_lc = setlocale (LC_CTYPE, NULL); if (old_lc) { old_lc = xtrystrdup (old_lc); if (!old_lc) return gpg_error_from_syserror (); } dft_lc = setlocale (LC_CTYPE, ""); #endif if (opt_lc_ctype || (dft_ttyname && dft_lc)) { err = send_one_option (ctx, errsource, "lc-ctype", opt_lc_ctype ? opt_lc_ctype : dft_lc, 0); } #if defined(HAVE_SETLOCALE) && defined(LC_CTYPE) if (old_lc) { setlocale (LC_CTYPE, old_lc); xfree (old_lc); } #endif if (err) return err; /* Send the value for LC_MESSAGES. */ #if defined(HAVE_SETLOCALE) && defined(LC_MESSAGES) old_lc = setlocale (LC_MESSAGES, NULL); if (old_lc) { old_lc = xtrystrdup (old_lc); if (!old_lc) return gpg_error_from_syserror (); } dft_lc = setlocale (LC_MESSAGES, ""); #endif if (opt_lc_messages || (dft_ttyname && dft_lc)) { err = send_one_option (ctx, errsource, "lc-messages", opt_lc_messages ? opt_lc_messages : dft_lc, 0); } #if defined(HAVE_SETLOCALE) && defined(LC_MESSAGES) if (old_lc) { setlocale (LC_MESSAGES, old_lc); xfree (old_lc); } #endif if (err) return err; return 0; } /* Lock a spawning process. The caller needs to provide the address of a variable to store the lock information and the name or the process. */ static gpg_error_t lock_spawning (lock_spawn_t *lock, const char *homedir, const char *name, int verbose) { char *fname; (void)verbose; *lock = NULL; fname = make_absfilename_try (homedir, !strcmp (name, "agent")? "gnupg_spawn_agent_sentinel": !strcmp (name, "dirmngr")? "gnupg_spawn_dirmngr_sentinel": !strcmp (name, "keyboxd")? "gnupg_spawn_keyboxd_sentinel": /* */ "gnupg_spawn_unknown_sentinel", NULL); if (!fname) return gpg_error_from_syserror (); *lock = dotlock_create (fname, 0); xfree (fname); if (!*lock) return gpg_error_from_syserror (); /* FIXME: We should use a timeout of 5000 here - however make_dotlock does not yet support values other than -1 and 0. */ if (dotlock_take (*lock, -1)) return gpg_error_from_syserror (); return 0; } /* Unlock the spawning process. */ static void unlock_spawning (lock_spawn_t *lock, const char *name) { if (*lock) { (void)name; dotlock_destroy (*lock); *lock = NULL; } } /* Helper to start a service. SECS gives the number of seconds to * wait. SOCKNAME is the name of the socket to connect. VERBOSE is * the usual verbose flag. CTX is the assuan context. CONNECT_FLAGS * are the assuan connect flags. DID_SUCCESS_MSG will be set to 1 if * a success messages has been printed. */ static gpg_error_t wait_for_sock (int secs, int module_name_id, const char *sockname, unsigned int connect_flags, int verbose, assuan_context_t ctx, int *did_success_msg) { gpg_error_t err = 0; int target_us = secs * 1000000; int elapsed_us = 0; /* * 977us * 1024 = just a little more than 1s. * so we will double this timeout 10 times in the first * second, and then switch over to 1s checkins. */ int next_sleep_us = 977; int lastalert = secs+1; int secsleft; while (elapsed_us < target_us) { if (verbose) { secsleft = (target_us - elapsed_us + 999999)/1000000; /* log_clock ("left=%d last=%d targ=%d elap=%d next=%d\n", */ /* secsleft, lastalert, target_us, elapsed_us, */ /* next_sleep_us); */ if (secsleft < lastalert) { log_info (module_name_id == GNUPG_MODULE_NAME_DIRMNGR? _("waiting for the dirmngr to come up ... (%ds)\n"): module_name_id == GNUPG_MODULE_NAME_KEYBOXD? _("waiting for the keyboxd to come up ... (%ds)\n"): _("waiting for the agent to come up ... (%ds)\n"), secsleft); lastalert = secsleft; } } gnupg_usleep (next_sleep_us); elapsed_us += next_sleep_us; err = assuan_socket_connect (ctx, sockname, 0, connect_flags); if (!err) { if (verbose) { log_info (module_name_id == GNUPG_MODULE_NAME_DIRMNGR? _("connection to the dirmngr established\n"): module_name_id == GNUPG_MODULE_NAME_KEYBOXD? _("connection to the keyboxd established\n"): _("connection to the agent established\n")); *did_success_msg = 1; } break; } next_sleep_us *= 2; if (next_sleep_us > 1000000) next_sleep_us = 1000000; } return err; } /* Try to connect to a new service via socket or start it if it is not * running and AUTOSTART is set. Handle the server's initial * greeting. Returns a new assuan context at R_CTX or an error code. * MODULE_NAME_ID is one of: * GNUPG_MODULE_NAME_AGENT * GNUPG_MODULE_NAME_DIRMNGR */ static gpg_error_t start_new_service (assuan_context_t *r_ctx, int module_name_id, gpg_err_source_t errsource, const char *program_name, const char *opt_lc_ctype, const char *opt_lc_messages, session_env_t session_env, int autostart, int verbose, int debug, gpg_error_t (*status_cb)(ctrl_t, int, ...), ctrl_t status_cb_arg) { gpg_error_t err; assuan_context_t ctx; int did_success_msg = 0; char *sockname; const char *printed_name; const char *lock_name; const char *status_start_line; int no_service_err; int seconds_to_wait; unsigned int connect_flags = 0; const char *argv[6]; *r_ctx = NULL; err = assuan_new (&ctx); if (err) { log_error ("error allocating assuan context: %s\n", gpg_strerror (err)); return err; } switch (module_name_id) { case GNUPG_MODULE_NAME_AGENT: sockname = make_filename (gnupg_socketdir (), GPG_AGENT_SOCK_NAME, NULL); lock_name = "agent"; printed_name = "gpg-agent"; status_start_line = "starting_agent ? 0 0"; no_service_err = GPG_ERR_NO_AGENT; seconds_to_wait = SECS_TO_WAIT_FOR_AGENT; break; case GNUPG_MODULE_NAME_DIRMNGR: sockname = make_filename (gnupg_socketdir (), DIRMNGR_SOCK_NAME, NULL); lock_name = "dirmngr"; printed_name = "dirmngr"; status_start_line = "starting_dirmngr ? 0 0"; no_service_err = GPG_ERR_NO_DIRMNGR; seconds_to_wait = SECS_TO_WAIT_FOR_DIRMNGR; break; case GNUPG_MODULE_NAME_KEYBOXD: sockname = make_filename (gnupg_socketdir (), KEYBOXD_SOCK_NAME, NULL); lock_name = "keyboxd"; printed_name = "keyboxd"; status_start_line = "starting_keyboxd ? 0 0"; no_service_err = GPG_ERR_NO_KEYBOXD; seconds_to_wait = SECS_TO_WAIT_FOR_KEYBOXD; connect_flags |= ASSUAN_SOCKET_CONNECT_FDPASSING; break; default: err = gpg_error (GPG_ERR_INV_ARG); assuan_release (ctx); return err; } err = assuan_socket_connect (ctx, sockname, 0, connect_flags); if (err && autostart) { char *abs_homedir; lock_spawn_t lock; char *program = NULL; const char *program_arg = NULL; char *p; const char *s; int i; /* With no success start a new server. */ if (!program_name || !*program_name) program_name = gnupg_module_name (module_name_id); else if ((s=strchr (program_name, '|')) && s[1] == '-' && s[2]=='-') { /* Hack to insert an additional option on the command line. */ program = xtrystrdup (program_name); if (!program) { gpg_error_t tmperr = gpg_err_make (errsource, gpg_err_code_from_syserror ()); xfree (sockname); assuan_release (ctx); return tmperr; } p = strchr (program, '|'); *p++ = 0; program_arg = p; } if (verbose) log_info (_("no running %s - starting '%s'\n"), printed_name, program_name); if (status_cb) status_cb (status_cb_arg, STATUS_PROGRESS, status_start_line, NULL); /* We better pass an absolute home directory to the service just * in case the service does not convert the passed name to an * absolute one (which it should do). */ abs_homedir = make_absfilename_try (gnupg_homedir (), NULL); if (!abs_homedir) { gpg_error_t tmperr = gpg_err_make (errsource, gpg_err_code_from_syserror ()); log_error ("error building filename: %s\n", gpg_strerror (tmperr)); xfree (sockname); assuan_release (ctx); xfree (program); return tmperr; } if (fflush (NULL)) { gpg_error_t tmperr = gpg_err_make (errsource, gpg_err_code_from_syserror ()); log_error ("error flushing pending output: %s\n", strerror (errno)); xfree (sockname); assuan_release (ctx); xfree (abs_homedir); xfree (program); return tmperr; } i = 0; argv[i++] = "--homedir"; argv[i++] = abs_homedir; if (module_name_id == GNUPG_MODULE_NAME_AGENT) argv[i++] = "--use-standard-socket"; if (program_arg) argv[i++] = program_arg; argv[i++] = "--daemon"; argv[i++] = NULL; if (!(err = lock_spawning (&lock, gnupg_homedir (), lock_name, verbose)) && assuan_socket_connect (ctx, sockname, 0, connect_flags)) { #ifdef HAVE_W32_SYSTEM - err = gnupg_spawn_process_detached (program? program : program_name, - argv, NULL); + err = gnupg_process_spawn (program? program : program_name, argv, + GNUPG_PROCESS_DETACHED, + NULL, NULL, NULL); #else /*!W32*/ - pid_t pid; - - err = gnupg_spawn_process_fd (program? program : program_name, - argv, -1, -1, -1, &pid); - if (!err) - err = gnupg_wait_process (program? program : program_name, - pid, 1, NULL); + err = gnupg_process_spawn (program? program : program_name, argv, + 0, NULL, NULL, NULL); #endif /*!W32*/ if (err) log_error ("failed to start %s '%s': %s\n", printed_name, program? program : program_name, gpg_strerror (err)); else err = wait_for_sock (seconds_to_wait, module_name_id, sockname, connect_flags, verbose, ctx, &did_success_msg); } unlock_spawning (&lock, lock_name); xfree (abs_homedir); xfree (program); } xfree (sockname); if (err) { if (autostart || gpg_err_code (err) != GPG_ERR_ASS_CONNECT_FAILED) log_error ("can't connect to the %s: %s\n", printed_name, gpg_strerror (err)); assuan_release (ctx); return gpg_err_make (errsource, no_service_err); } if (debug && !did_success_msg) log_debug ("connection to the %s established\n", printed_name); if (module_name_id == GNUPG_MODULE_NAME_AGENT) err = assuan_transact (ctx, "RESET", NULL, NULL, NULL, NULL, NULL, NULL); if (!err && module_name_id == GNUPG_MODULE_NAME_AGENT) { err = send_pinentry_environment (ctx, errsource, opt_lc_ctype, opt_lc_messages, session_env); if (gpg_err_code (err) == GPG_ERR_FORBIDDEN && gpg_err_source (err) == GPG_ERR_SOURCE_GPGAGENT) { /* Check whether the agent is in restricted mode. */ if (!assuan_transact (ctx, "GETINFO restricted", NULL, NULL, NULL, NULL, NULL, NULL)) { if (verbose) log_info (_("connection to the agent is in restricted mode\n")); err = 0; } } } if (err) { assuan_release (ctx); return err; } *r_ctx = ctx; return 0; } /* Try to connect to the agent or start a new one. */ gpg_error_t start_new_gpg_agent (assuan_context_t *r_ctx, gpg_err_source_t errsource, const char *agent_program, const char *opt_lc_ctype, const char *opt_lc_messages, session_env_t session_env, int autostart, int verbose, int debug, gpg_error_t (*status_cb)(ctrl_t, int, ...), ctrl_t status_cb_arg) { return start_new_service (r_ctx, GNUPG_MODULE_NAME_AGENT, errsource, agent_program, opt_lc_ctype, opt_lc_messages, session_env, autostart, verbose, debug, status_cb, status_cb_arg); } /* Try to connect to the dirmngr via a socket. On platforms supporting it, start it up if needed and if AUTOSTART is true. Returns a new assuan context at R_CTX or an error code. */ gpg_error_t start_new_keyboxd (assuan_context_t *r_ctx, gpg_err_source_t errsource, const char *keyboxd_program, int autostart, int verbose, int debug, gpg_error_t (*status_cb)(ctrl_t, int, ...), ctrl_t status_cb_arg) { return start_new_service (r_ctx, GNUPG_MODULE_NAME_KEYBOXD, errsource, keyboxd_program, NULL, NULL, NULL, autostart, verbose, debug, status_cb, status_cb_arg); } /* Try to connect to the dirmngr via a socket. On platforms supporting it, start it up if needed and if AUTOSTART is true. Returns a new assuan context at R_CTX or an error code. */ gpg_error_t start_new_dirmngr (assuan_context_t *r_ctx, gpg_err_source_t errsource, const char *dirmngr_program, int autostart, int verbose, int debug, gpg_error_t (*status_cb)(ctrl_t, int, ...), ctrl_t status_cb_arg) { #ifndef USE_DIRMNGR_AUTO_START autostart = 0; #endif return start_new_service (r_ctx, GNUPG_MODULE_NAME_DIRMNGR, errsource, dirmngr_program, NULL, NULL, NULL, autostart, verbose, debug, status_cb, status_cb_arg); } /* Return the version of a server using "GETINFO version". On success 0 is returned and R_VERSION receives a malloced string with the version which must be freed by the caller. On error NULL is stored at R_VERSION and an error code returned. Mode is in general 0 but certain values may be used to modify the used version command: MODE == 0 = Use "GETINFO version" MODE == 2 - Use "SCD GETINFO version" */ gpg_error_t get_assuan_server_version (assuan_context_t ctx, int mode, char **r_version) { gpg_error_t err; membuf_t data; init_membuf (&data, 64); err = assuan_transact (ctx, mode == 2? "SCD GETINFO version" /**/ : "GETINFO version", put_membuf_cb, &data, NULL, NULL, NULL, NULL); if (err) { xfree (get_membuf (&data, NULL)); *r_version = NULL; } else { put_membuf (&data, "", 1); *r_version = get_membuf (&data, NULL); if (!*r_version) err = gpg_error_from_syserror (); } return err; } /* Print a warning if the server's version number is less than our * version number. Returns an error code on a connection problem. * CTX is the Assuan context, SERVERNAME is the name of teh server, * STATUS_FUNC and STATUS_FUNC_DATA is a callback to emit status * messages. If PRINT_HINTS is set additional hints are printed. For * MODE see get_assuan_server_version. */ gpg_error_t warn_server_version_mismatch (assuan_context_t ctx, const char *servername, int mode, gpg_error_t (*status_func)(ctrl_t ctrl, int status_no, ...), void *status_func_ctrl, int print_hints) { gpg_error_t err; char *serverversion; const char *myversion = gpgrt_strusage (13); err = get_assuan_server_version (ctx, mode, &serverversion); if (err) log_log (gpg_err_code (err) == GPG_ERR_NOT_SUPPORTED? GPGRT_LOGLVL_INFO : GPGRT_LOGLVL_ERROR, _("error getting version from '%s': %s\n"), servername, gpg_strerror (err)); else if (compare_version_strings (serverversion, myversion) < 0) { char *warn; warn = xtryasprintf (_("server '%s' is older than us (%s < %s)"), servername, serverversion, myversion); if (!warn) err = gpg_error_from_syserror (); else { log_info (_("WARNING: %s\n"), warn); if (print_hints) { log_info (_("Note: Outdated servers may lack important" " security fixes.\n")); log_info (_("Note: Use the command \"%s\" to restart them.\n"), "gpgconf --kill all"); } if (status_func) status_func (status_func_ctrl, STATUS_WARNING, "server_version_mismatch 0", warn, NULL); xfree (warn); } } xfree (serverversion); return err; } diff --git a/common/exechelp-posix.c b/common/exechelp-posix.c index fa613449d..7b20a3796 100644 --- a/common/exechelp-posix.c +++ b/common/exechelp-posix.c @@ -1,917 +1,1032 @@ /* exechelp.c - Fork and exec helpers for POSIX * Copyright (C) 2004, 2007-2009, 2010 Free Software Foundation, Inc. * Copyright (C) 2004, 2006-2012, 2014-2017 g10 Code GmbH * * This file is part of GnuPG. * * This file is free software; you can redistribute it and/or modify * it under the terms of either * * - the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at * your option) any later version. * * or * * - the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at * your option) any later version. * * or both in parallel, as here. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * SPDX-License-Identifier: (LGPL-3.0+ OR GPL-2.0+) */ #include #if defined(HAVE_W32_SYSTEM) #error This code is only used on POSIX #endif #include #include #include #include #include #include #ifdef HAVE_SIGNAL_H # include #endif #include #include #ifdef WITHOUT_NPTH /* Give the Makefile a chance to build without Pth. */ #undef HAVE_NPTH #undef USE_NPTH #endif #ifdef HAVE_NPTH #include #endif #include #ifdef HAVE_GETRLIMIT #include #include #endif /*HAVE_GETRLIMIT*/ #ifdef HAVE_STAT # include #endif #if __linux__ # include # include #endif /*__linux__ */ #include "util.h" #include "i18n.h" #include "sysutils.h" #include "exechelp.h" /* Helper */ static inline gpg_error_t my_error_from_syserror (void) { return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); } static inline gpg_error_t my_error (int errcode) { return gpg_err_make (default_errsource, errcode); } /* Return the maximum number of currently allowed open file descriptors. Only useful on POSIX systems but returns a value on other systems too. */ int get_max_fds (void) { int max_fds = -1; #ifdef HAVE_GETRLIMIT struct rlimit rl; /* Under Linux we can figure out the highest used file descriptor by * reading /proc/PID/fd. This is in the common cases much fast than * for example doing 4096 close calls where almost all of them will * fail. On a system with a limit of 4096 files and only 8 files * open with the highest number being 10, we speedup close_all_fds * from 125ms to 0.4ms including readdir. * * Another option would be to close the file descriptors as returned * from reading that directory - however then we need to snapshot * that list before starting to close them. */ #ifdef __linux__ { DIR *dir = NULL; struct dirent *dir_entry; const char *s; int x; dir = opendir ("/proc/self/fd"); if (dir) { while ((dir_entry = readdir (dir))) { s = dir_entry->d_name; if ( *s < '0' || *s > '9') continue; x = atoi (s); if (x > max_fds) max_fds = x; } closedir (dir); } if (max_fds != -1) return max_fds + 1; } #endif /* __linux__ */ # ifdef RLIMIT_NOFILE if (!getrlimit (RLIMIT_NOFILE, &rl)) max_fds = rl.rlim_max; # endif # ifdef RLIMIT_OFILE if (max_fds == -1 && !getrlimit (RLIMIT_OFILE, &rl)) max_fds = rl.rlim_max; # endif #endif /*HAVE_GETRLIMIT*/ #ifdef _SC_OPEN_MAX if (max_fds == -1) { long int scres = sysconf (_SC_OPEN_MAX); if (scres >= 0) max_fds = scres; } #endif #ifdef _POSIX_OPEN_MAX if (max_fds == -1) max_fds = _POSIX_OPEN_MAX; #endif #ifdef OPEN_MAX if (max_fds == -1) max_fds = OPEN_MAX; #endif if (max_fds == -1) max_fds = 256; /* Arbitrary limit. */ /* AIX returns INT32_MAX instead of a proper value. We assume that this is always an error and use an arbitrary limit. */ #ifdef INT32_MAX if (max_fds == INT32_MAX) max_fds = 256; #endif return max_fds; } /* Close all file descriptors starting with descriptor FIRST. If EXCEPT is not NULL, it is expected to be a list of file descriptors which shall not be closed. This list shall be sorted in ascending order with the end marked by -1. */ void close_all_fds (int first, int *except) { int max_fd = get_max_fds (); int fd, i, except_start; if (except) { except_start = 0; for (fd=first; fd < max_fd; fd++) { for (i=except_start; except[i] != -1; i++) { if (except[i] == fd) { /* If we found the descriptor in the exception list we can start the next compare run at the next index because the exception list is ordered. */ except_start = i + 1; break; } } if (except[i] == -1) close (fd); } } else { for (fd=first; fd < max_fd; fd++) close (fd); } gpg_err_set_errno (0); } /* Returns an array with all currently open file descriptors. The end of the array is marked by -1. The caller needs to release this array using the *standard free* and not with xfree. This allow the use of this function right at startup even before libgcrypt has been initialized. Returns NULL on error and sets ERRNO accordingly. */ int * get_all_open_fds (void) { int *array; size_t narray; int fd, max_fd, idx; #ifndef HAVE_STAT array = calloc (1, sizeof *array); if (array) array[0] = -1; #else /*HAVE_STAT*/ struct stat statbuf; max_fd = get_max_fds (); narray = 32; /* If you change this change also t-exechelp.c. */ array = calloc (narray, sizeof *array); if (!array) return NULL; /* Note: The list we return is ordered. */ for (idx=0, fd=0; fd < max_fd; fd++) if (!(fstat (fd, &statbuf) == -1 && errno == EBADF)) { if (idx+1 >= narray) { int *tmp; narray += (narray < 256)? 32:256; tmp = realloc (array, narray * sizeof *array); if (!tmp) { free (array); return NULL; } array = tmp; } array[idx++] = fd; } array[idx] = -1; #endif /*HAVE_STAT*/ return array; } -/* The exec core used right after the fork. This will never return. */ -static void -do_exec (const char *pgmname, const char *argv[], - int fd_in, int fd_out, int fd_err, - int *except, unsigned int flags) -{ - char **arg_list; - int i, j; - int fds[3]; - int nodevnull[3]; - - fds[0] = fd_in; - fds[1] = fd_out; - fds[2] = fd_err; - - nodevnull[0] = !!(flags & GNUPG_SPAWN_KEEP_STDIN); - nodevnull[1] = !!(flags & GNUPG_SPAWN_KEEP_STDOUT); - nodevnull[2] = !!(flags & GNUPG_SPAWN_KEEP_STDERR); - - /* Create the command line argument array. */ - i = 0; - if (argv) - while (argv[i]) - i++; - arg_list = xcalloc (i+2, sizeof *arg_list); - arg_list[0] = strrchr (pgmname, '/'); - if (arg_list[0]) - arg_list[0]++; - else - arg_list[0] = xstrdup (pgmname); - if (argv) - for (i=0,j=1; argv[i]; i++, j++) - arg_list[j] = (char*)argv[i]; - - /* Assign /dev/null to unused FDs. */ - for (i=0; i <= 2; i++) - { - if (nodevnull[i]) - continue; - if (fds[i] == -1) - { - fds[i] = open ("/dev/null", i? O_WRONLY : O_RDONLY); - if (fds[i] == -1) - log_fatal ("failed to open '%s': %s\n", - "/dev/null", strerror (errno)); - } - } - - /* Connect the standard files. */ - for (i=0; i <= 2; i++) - { - if (nodevnull[i]) - continue; - if (fds[i] != i && dup2 (fds[i], i) == -1) - log_fatal ("dup2 std%s failed: %s\n", - i==0?"in":i==1?"out":"err", strerror (errno)); - } - - /* Close all other files. */ - close_all_fds (3, except); - - execv (pgmname, arg_list); - /* No way to print anything, as we have closed all streams. */ - _exit (127); -} - - static gpg_error_t do_create_pipe (int filedes[2]) { gpg_error_t err = 0; if (pipe (filedes) == -1) { err = my_error_from_syserror (); filedes[0] = filedes[1] = -1; } return err; } static gpg_error_t create_pipe_and_estream (int filedes[2], estream_t *r_fp, int outbound, int nonblock) { gpg_error_t err; if (pipe (filedes) == -1) { err = my_error_from_syserror (); log_error (_("error creating a pipe: %s\n"), gpg_strerror (err)); filedes[0] = filedes[1] = -1; *r_fp = NULL; return err; } if (!outbound) *r_fp = es_fdopen (filedes[0], nonblock? "r,nonblock" : "r"); else *r_fp = es_fdopen (filedes[1], nonblock? "w,nonblock" : "w"); if (!*r_fp) { err = my_error_from_syserror (); log_error (_("error creating a stream for a pipe: %s\n"), gpg_strerror (err)); close (filedes[0]); close (filedes[1]); filedes[0] = filedes[1] = -1; return err; } return 0; } /* Portable function to create a pipe. Under Windows the write end is inheritable. If R_FP is not NULL, an estream is created for the read end and stored at R_FP. */ gpg_error_t gnupg_create_inbound_pipe (int filedes[2], estream_t *r_fp, int nonblock) { if (r_fp) return create_pipe_and_estream (filedes, r_fp, 0, nonblock); else return do_create_pipe (filedes); } /* Portable function to create a pipe. Under Windows the read end is inheritable. If R_FP is not NULL, an estream is created for the write end and stored at R_FP. */ gpg_error_t gnupg_create_outbound_pipe (int filedes[2], estream_t *r_fp, int nonblock) { if (r_fp) return create_pipe_and_estream (filedes, r_fp, 1, nonblock); else return do_create_pipe (filedes); } /* Portable function to create a pipe. Under Windows both ends are inheritable. */ gpg_error_t gnupg_create_pipe (int filedes[2]) { return do_create_pipe (filedes); } /* Close the end of a pipe. */ void gnupg_close_pipe (int fd) { if (fd != -1) close (fd); } +#include -/* Fork and exec the PGMNAME, see exechelp.h for details. */ -gpg_error_t -gnupg_spawn_process (const char *pgmname, const char *argv[], - int *except, unsigned int flags, - estream_t *r_infp, - estream_t *r_outfp, - estream_t *r_errfp, - pid_t *pid) -{ - gpg_error_t err; - int inpipe[2] = {-1, -1}; - int outpipe[2] = {-1, -1}; - int errpipe[2] = {-1, -1}; - estream_t infp = NULL; - estream_t outfp = NULL; - estream_t errfp = NULL; - int nonblock = !!(flags & GNUPG_SPAWN_NONBLOCK); +struct gnupg_process { + const char *pgmname; + unsigned int terminated :1; /* or detached */ + unsigned int flags; + pid_t pid; + int fd_in; + int fd_out; + int fd_err; + int wstatus; +}; - if (r_infp) - *r_infp = NULL; - if (r_outfp) - *r_outfp = NULL; - if (r_errfp) - *r_errfp = NULL; - *pid = (pid_t)(-1); /* Always required. */ +static int gnupg_process_syscall_func_initialized; - if (r_infp) - { - err = create_pipe_and_estream (inpipe, &infp, 1, nonblock); - if (err) - return err; - } +/* Functions called before and after blocking syscalls. */ +static void (*pre_syscall_func) (void); +static void (*post_syscall_func) (void); - if (r_outfp) +static void +check_syscall_func (void) +{ + if (!gnupg_process_syscall_func_initialized) { - err = create_pipe_and_estream (outpipe, &outfp, 0, nonblock); - if (err) - { - if (infp) - es_fclose (infp); - else if (inpipe[1] != -1) - close (inpipe[1]); - if (inpipe[0] != -1) - close (inpipe[0]); - - return err; - } + gpgrt_get_syscall_clamp (&pre_syscall_func, &post_syscall_func); + gnupg_process_syscall_func_initialized = 1; } +} - if (r_errfp) - { - err = create_pipe_and_estream (errpipe, &errfp, 0, nonblock); - if (err) - { - if (infp) - es_fclose (infp); - else if (inpipe[1] != -1) - close (inpipe[1]); - if (inpipe[0] != -1) - close (inpipe[0]); - - if (outfp) - es_fclose (outfp); - else if (outpipe[0] != -1) - close (outpipe[0]); - if (outpipe[1] != -1) - close (outpipe[1]); - - return err; - } - } +static void +pre_syscall (void) +{ + if (pre_syscall_func) + pre_syscall_func (); +} + +static void +post_syscall (void) +{ + if (post_syscall_func) + post_syscall_func (); +} - *pid = fork (); - if (*pid == (pid_t)(-1)) - { - err = my_error_from_syserror (); - log_error (_("error forking process: %s\n"), gpg_strerror (err)); - - if (infp) - es_fclose (infp); - else if (inpipe[1] != -1) - close (inpipe[1]); - if (inpipe[0] != -1) - close (inpipe[0]); - - if (outfp) - es_fclose (outfp); - else if (outpipe[0] != -1) - close (outpipe[0]); - if (outpipe[1] != -1) - close (outpipe[1]); - - if (errfp) - es_fclose (errfp); - else if (errpipe[0] != -1) - close (errpipe[0]); - if (errpipe[1] != -1) - close (errpipe[1]); - return err; - } +static gpg_err_code_t +do_create_socketpair (int filedes[2]) +{ + gpg_error_t err = 0; - if (!*pid) + pre_syscall (); + if (socketpair (AF_LOCAL, SOCK_STREAM, 0, filedes) == -1) { - /* This is the child. */ - gcry_control (GCRYCTL_TERM_SECMEM); - es_fclose (infp); - es_fclose (outfp); - es_fclose (errfp); - do_exec (pgmname, argv, inpipe[0], outpipe[1], errpipe[1], - except, flags); - /*NOTREACHED*/ + err = gpg_err_code_from_syserror (); + filedes[0] = filedes[1] = -1; } + post_syscall (); - /* This is the parent. */ - if (inpipe[0] != -1) - close (inpipe[0]); - if (outpipe[1] != -1) - close (outpipe[1]); - if (errpipe[1] != -1) - close (errpipe[1]); + return err; +} - if (r_infp) - *r_infp = infp; - if (r_outfp) - *r_outfp = outfp; - if (r_errfp) - *r_errfp = errfp; +static int +posix_open_null (int for_write) +{ + int fd; - return 0; + fd = open ("/dev/null", for_write? O_WRONLY : O_RDONLY); + if (fd == -1) + log_fatal ("failed to open '/dev/null': %s\n", strerror (errno)); + return fd; } +static void +call_spawn_cb (struct spawn_cb_arg *sca, + int fd_in, int fd_out, int fd_err, + void (*spawn_cb) (struct spawn_cb_arg *), void *spawn_cb_arg) +{ + sca->fds[0] = fd_in; + sca->fds[1] = fd_out; + sca->fds[2] = fd_err; + sca->except_fds = NULL; + sca->arg = spawn_cb_arg; + if (spawn_cb) + (*spawn_cb) (sca); +} +static void +my_exec (const char *pgmname, const char *argv[], struct spawn_cb_arg *sca) +{ + int i; -/* Simplified version of gnupg_spawn_process. This function forks and - then execs PGMNAME, while connecting INFD to stdin, OUTFD to stdout - and ERRFD to stderr (any of them may be -1 to connect them to - /dev/null). The arguments for the process are expected in the NULL - terminated array ARGV. The program name itself should not be - included there. Calling gnupg_wait_process is required. + /* Assign /dev/null to unused FDs. */ + for (i = 0; i <= 2; i++) + if (sca->fds[i] == -1) + sca->fds[i] = posix_open_null (i); - Returns 0 on success or an error code. */ -gpg_error_t -gnupg_spawn_process_fd (const char *pgmname, const char *argv[], - int infd, int outfd, int errfd, pid_t *pid) + /* Connect the standard files. */ + for (i = 0; i <= 2; i++) + if (sca->fds[i] != i) + { + if (dup2 (sca->fds[i], i) == -1) + log_fatal ("dup2 std%s failed: %s\n", + i==0?"in":i==1?"out":"err", strerror (errno)); + /* + * We don't close sca.fds[i] here, but close them by + * close_all_fds. Note that there may be same one in three of + * sca->fds[i]. + */ + } + + /* Close all other files. */ + close_all_fds (3, sca->except_fds); + + execv (pgmname, (char *const *)argv); + /* No way to print anything, as we have may have closed all streams. */ + _exit (127); +} + +static gpg_err_code_t +spawn_detached (const char *pgmname, const char *argv[], + void (*spawn_cb) (struct spawn_cb_arg *), void *spawn_cb_arg) { - gpg_error_t err; + gpg_err_code_t ec; + pid_t pid; - *pid = fork (); - if (*pid == (pid_t)(-1)) + /* FIXME: Is this GnuPG specific or should we keep it. */ + if (getuid() != geteuid()) { - err = my_error_from_syserror (); - log_error (_("error forking process: %s\n"), strerror (errno)); - return err; + xfree (argv); + return GPG_ERR_BUG; } - if (!*pid) + if (access (pgmname, X_OK)) { - gcry_control (GCRYCTL_TERM_SECMEM); - /* Run child. */ - do_exec (pgmname, argv, infd, outfd, errfd, NULL, 0); - /*NOTREACHED*/ + ec = gpg_err_code_from_syserror (); + xfree (argv); + return ec; } - return 0; -} - - - + pre_syscall (); + pid = fork (); + post_syscall (); + if (pid == (pid_t)(-1)) + { + ec = gpg_err_code_from_syserror (); + log_error (_("error forking process: %s\n"), gpg_strerror (ec)); + xfree (argv); + return ec; + } -/* Waiting for child processes. + if (!pid) + { + pid_t pid2; + struct spawn_cb_arg sca; - waitpid(2) may return information about terminated children that we - did not yet request, and there is no portable way to wait for a - specific set of children. + if (setsid() == -1 || chdir ("/")) + _exit (1); - As a workaround, we store the results of children for later use. + pid2 = fork (); /* Double fork to let init take over the new child. */ + if (pid2 == (pid_t)(-1)) + _exit (1); + if (pid2) + _exit (0); /* Let the parent exit immediately. */ - XXX: This assumes that PIDs are not reused too quickly. */ + call_spawn_cb (&sca, -1, -1, -1, spawn_cb, spawn_cb_arg); -struct terminated_child -{ - pid_t pid; - int exitcode; - struct terminated_child *next; -}; + my_exec (pgmname, argv, &sca); + /*NOTREACHED*/ + } -struct terminated_child *terminated_children; + pre_syscall (); + if (waitpid (pid, NULL, 0) == -1) + { + post_syscall (); + ec = gpg_err_code_from_syserror (); + log_error ("waitpid failed in gpgrt_spawn_process_detached: %s", + gpg_strerror (ec)); + return ec; + } + else + post_syscall (); + return 0; +} -static gpg_error_t -store_result (pid_t pid, int exitcode) +void +gnupg_spawn_helper (struct spawn_cb_arg *sca) { - struct terminated_child *c; + int *user_except = sca->arg; + sca->except_fds = user_except; +} - c = xtrymalloc (sizeof *c); - if (c == NULL) - return gpg_err_code_from_syserror (); +gpg_err_code_t +gnupg_process_spawn (const char *pgmname, const char *argv1[], + unsigned int flags, + void (*spawn_cb) (struct spawn_cb_arg *), + void *spawn_cb_arg, + gnupg_process_t *r_process) +{ + gpg_err_code_t ec; + gnupg_process_t process; + int fd_in[2]; + int fd_out[2]; + int fd_err[2]; + pid_t pid; + const char **argv; + int i, j; - c->pid = pid; - c->exitcode = exitcode; - c->next = terminated_children; - terminated_children = c; + check_syscall_func (); - return 0; -} + if (r_process) + *r_process = NULL; + /* Create the command line argument array. */ + i = 0; + if (argv1) + while (argv1[i]) + i++; + argv = xtrycalloc (i+2, sizeof *argv); + if (!argv) + return gpg_err_code_from_syserror (); + argv[0] = strrchr (pgmname, '/'); + if (argv[0]) + argv[0]++; + else + argv[0] = pgmname; -static int -get_result (pid_t pid, int *r_exitcode) -{ - struct terminated_child *c, **prevp; + if (argv1) + for (i=0, j=1; argv1[i]; i++, j++) + argv[j] = argv1[i]; - for (prevp = &terminated_children, c = terminated_children; - c; - prevp = &c->next, c = c->next) - if (c->pid == pid) - { - *prevp = c->next; - *r_exitcode = c->exitcode; - xfree (c); - return 1; - } + if ((flags & GNUPG_PROCESS_DETACHED)) + { + if ((flags & GNUPG_PROCESS_STDFDS_SETTING)) + { + xfree (argv); + return GPG_ERR_INV_FLAG; + } - return 0; -} + /* In detached case, it must be no R_PROCESS. */ + if (r_process) + { + xfree (argv); + return GPG_ERR_INV_ARG; + } + return spawn_detached (pgmname, argv, spawn_cb, spawn_cb_arg); + } -/* See exechelp.h for a description. */ -gpg_error_t -gnupg_wait_process (const char *pgmname, pid_t pid, int hang, int *r_exitcode) -{ - gpg_err_code_t ec; - int i, status; + process = xtrycalloc (1, sizeof (struct gnupg_process)); + if (process == NULL) + { + xfree (argv); + return gpg_err_code_from_syserror (); + } - if (r_exitcode) - *r_exitcode = -1; + process->pgmname = pgmname; + process->flags = flags; - if (pid == (pid_t)(-1)) - return gpg_error (GPG_ERR_INV_VALUE); + if ((flags & GNUPG_PROCESS_STDINOUT_SOCKETPAIR)) + { + ec = do_create_socketpair (fd_in); + if (ec) + { + xfree (process); + xfree (argv); + return ec; + } + fd_out[0] = dup (fd_in[0]); + fd_out[1] = dup (fd_in[1]); + } + else + { + if ((flags & GNUPG_PROCESS_STDIN_PIPE)) + { + ec = do_create_pipe (fd_in); + if (ec) + { + xfree (process); + xfree (argv); + return ec; + } + } + else if ((flags & GNUPG_PROCESS_STDIN_KEEP)) + { + fd_in[0] = 0; + fd_in[1] = -1; + } + else + { + fd_in[0] = -1; + fd_in[1] = -1; + } -#ifdef USE_NPTH - i = npth_waitpid (pid, &status, hang? 0:WNOHANG); -#else - while ((i=waitpid (pid, &status, hang? 0:WNOHANG)) == (pid_t)(-1) - && errno == EINTR); -#endif + if ((flags & GNUPG_PROCESS_STDOUT_PIPE)) + { + ec = do_create_pipe (fd_out); + if (ec) + { + if (fd_in[0] >= 0 && fd_in[0] != 0) + close (fd_in[0]); + if (fd_in[1] >= 0) + close (fd_in[1]); + xfree (process); + xfree (argv); + return ec; + } + } + else if ((flags & GNUPG_PROCESS_STDOUT_KEEP)) + { + fd_out[0] = -1; + fd_out[1] = 1; + } + else + { + fd_out[0] = -1; + fd_out[1] = -1; + } + } - if (i == (pid_t)(-1)) + if ((flags & GNUPG_PROCESS_STDERR_PIPE)) { - ec = gpg_err_code_from_errno (errno); - log_error (_("waiting for process %d to terminate failed: %s\n"), - (int)pid, strerror (errno)); + ec = do_create_pipe (fd_err); + if (ec) + { + if (fd_in[0] >= 0 && fd_in[0] != 0) + close (fd_in[0]); + if (fd_in[1] >= 0) + close (fd_in[1]); + if (fd_out[0] >= 0) + close (fd_out[0]); + if (fd_out[1] >= 0 && fd_out[1] != 1) + close (fd_out[1]); + xfree (process); + xfree (argv); + return ec; + } } - else if (!i) + else if ((flags & GNUPG_PROCESS_STDERR_KEEP)) { - ec = GPG_ERR_TIMEOUT; /* Still running. */ + fd_err[0] = -1; + fd_err[1] = 2; } - else if (WIFEXITED (status) && WEXITSTATUS (status) == 127) + else { - log_error (_("error running '%s': probably not installed\n"), pgmname); - ec = GPG_ERR_CONFIGURATION; + fd_err[0] = -1; + fd_err[1] = -1; } - else if (WIFEXITED (status) && WEXITSTATUS (status)) + + pre_syscall (); + pid = fork (); + post_syscall (); + if (pid == (pid_t)(-1)) { - if (!r_exitcode) - log_error (_("error running '%s': exit status %d\n"), pgmname, - WEXITSTATUS (status)); - else - *r_exitcode = WEXITSTATUS (status); - ec = GPG_ERR_GENERAL; + ec = gpg_err_code_from_syserror (); + log_error (_("error forking process: %s\n"), gpg_strerror (ec)); + if (fd_in[0] >= 0 && fd_in[0] != 0) + close (fd_in[0]); + if (fd_in[1] >= 0) + close (fd_in[1]); + if (fd_out[0] >= 0) + close (fd_out[0]); + if (fd_out[1] >= 0 && fd_out[1] != 1) + close (fd_out[1]); + if (fd_err[0] >= 0) + close (fd_err[0]); + if (fd_err[1] >= 0 && fd_err[1] != 2) + close (fd_err[1]); + xfree (process); + xfree (argv); + return ec; } - else if (!WIFEXITED (status)) + + if (!pid) { - log_error (_("error running '%s': terminated\n"), pgmname); - ec = GPG_ERR_GENERAL; + struct spawn_cb_arg sca; + + if (fd_in[1] >= 0) + close (fd_in[1]); + if (fd_out[0] >= 0) + close (fd_out[0]); + if (fd_err[0] >= 0) + close (fd_err[0]); + + call_spawn_cb (&sca, fd_in[0], fd_out[1], fd_err[1], + spawn_cb, spawn_cb_arg); + + /* Run child. */ + my_exec (pgmname, argv, &sca); + /*NOTREACHED*/ } - else + + xfree (argv); + process->pid = pid; + + if (fd_in[0] >= 0 && fd_in[0] != 0) + close (fd_in[0]); + if (fd_out[1] >= 0 && fd_out[1] != 1) + close (fd_out[1]); + if (fd_err[1] >= 0 && fd_err[1] != 2) + close (fd_err[1]); + process->fd_in = fd_in[1]; + process->fd_out = fd_out[0]; + process->fd_err = fd_err[0]; + process->wstatus = -1; + process->terminated = 0; + + if (r_process == NULL) { - if (r_exitcode) - *r_exitcode = 0; - ec = 0; + ec = gnupg_process_wait (process, 1); + gnupg_process_release (process); + return ec; } - return gpg_err_make (GPG_ERR_SOURCE_DEFAULT, ec); + *r_process = process; + return 0; } -/* See exechelp.h for a description. */ -gpg_error_t -gnupg_wait_processes (const char **pgmnames, pid_t *pids, size_t count, - int hang, int *r_exitcodes) +static gpg_err_code_t +process_kill (gnupg_process_t process, int sig) { gpg_err_code_t ec = 0; - size_t i, left; - int *dummy = NULL; + pid_t pid = process->pid; - if (r_exitcodes == NULL) + pre_syscall (); + if (kill (pid, sig) < 0) + ec = gpg_err_code_from_syserror (); + post_syscall (); + return ec; +} + +gpg_err_code_t +gnupg_process_terminate (gnupg_process_t process) +{ + return process_kill (process, SIGTERM); +} + +gpg_err_code_t +gnupg_process_get_fds (gnupg_process_t process, unsigned int flags, + int *r_fd_in, int *r_fd_out, int *r_fd_err) +{ + (void)flags; + if (r_fd_in) { - dummy = r_exitcodes = xtrymalloc (sizeof *r_exitcodes * count); - if (dummy == NULL) - return gpg_err_code_from_syserror (); + *r_fd_in = process->fd_in; + process->fd_in = -1; } - - for (i = 0, left = count; i < count; i++) + if (r_fd_out) { - int status = -1; + *r_fd_out = process->fd_out; + process->fd_out = -1; + } + if (r_fd_err) + { + *r_fd_err = process->fd_err; + process->fd_err = -1; + } - /* Skip invalid PID. */ - if (pids[i] == (pid_t)(-1)) - { - r_exitcodes[i] = -1; - left -= 1; - continue; - } + return 0; +} - /* See if there was a previously stored result for this pid. */ - if (get_result (pids[i], &status)) - left -= 1; +gpg_err_code_t +gnupg_process_get_streams (gnupg_process_t process, unsigned int flags, + gpgrt_stream_t *r_fp_in, gpgrt_stream_t *r_fp_out, + gpgrt_stream_t *r_fp_err) +{ + int nonblock = (flags & GNUPG_PROCESS_STREAM_NONBLOCK)? 1: 0; - r_exitcodes[i] = status; + if (r_fp_in) + { + *r_fp_in = es_fdopen (process->fd_in, nonblock? "w,nonblock" : "w"); + process->fd_in = -1; + } + if (r_fp_out) + { + *r_fp_out = es_fdopen (process->fd_out, nonblock? "r,nonblock" : "r"); + process->fd_out = -1; + } + if (r_fp_err) + { + *r_fp_err = es_fdopen (process->fd_err, nonblock? "r,nonblock" : "r"); + process->fd_err = -1; } + return 0; +} - while (left > 0) +static gpg_err_code_t +process_vctl (gnupg_process_t process, unsigned int request, va_list arg_ptr) +{ + switch (request) { - pid_t pid; - int status; + case GNUPG_PROCESS_NOP: + return 0; -#ifdef USE_NPTH - pid = npth_waitpid (-1, &status, hang ? 0 : WNOHANG); -#else - while ((pid = waitpid (-1, &status, hang ? 0 : WNOHANG)) == (pid_t)(-1) - && errno == EINTR); -#endif + case GNUPG_PROCESS_GET_PROC_ID: + { + int *r_id = va_arg (arg_ptr, int *); - if (pid == (pid_t)(-1)) - { - ec = gpg_err_code_from_errno (errno); - log_error (_("waiting for processes to terminate failed: %s\n"), - strerror (errno)); - break; - } - else if (!pid) - { - ec = GPG_ERR_TIMEOUT; /* Still running. */ - break; - } - else - { - for (i = 0; i < count; i++) - if (pid == pids[i]) - break; + if (r_id == NULL) + return GPG_ERR_INV_VALUE; - if (i == count) - { - /* No match, store this result. */ - ec = store_result (pid, status); - if (ec) - break; - continue; - } + *r_id = (int)process->pid; + return 0; + } - /* Process PIDS[i] died. */ - if (r_exitcodes[i] != (pid_t) -1) - { - log_error ("PID %d was reused", pid); - ec = GPG_ERR_GENERAL; - break; - } + case GNUPG_PROCESS_GET_EXIT_ID: + { + int status = process->wstatus; + int *r_exit_status = va_arg (arg_ptr, int *); - left -= 1; - r_exitcodes[i] = status; - } - } + if (!process->terminated) + return GPG_ERR_UNFINISHED; - for (i = 0; i < count; i++) - { - if (r_exitcodes[i] == -1) - continue; + if (WIFEXITED (status)) + { + if (r_exit_status) + *r_exit_status = WEXITSTATUS (status); + } + else + *r_exit_status = -1; - if (WIFEXITED (r_exitcodes[i]) && WEXITSTATUS (r_exitcodes[i]) == 127) - { - log_error (_("error running '%s': probably not installed\n"), - pgmnames[i]); - ec = GPG_ERR_CONFIGURATION; - } - else if (WIFEXITED (r_exitcodes[i]) && WEXITSTATUS (r_exitcodes[i])) - { - if (dummy) - log_error (_("error running '%s': exit status %d\n"), - pgmnames[i], WEXITSTATUS (r_exitcodes[i])); - else - r_exitcodes[i] = WEXITSTATUS (r_exitcodes[i]); - ec = GPG_ERR_GENERAL; - } - else if (!WIFEXITED (r_exitcodes[i])) - { - log_error (_("error running '%s': terminated\n"), pgmnames[i]); - ec = GPG_ERR_GENERAL; - } + return 0; + } + + case GNUPG_PROCESS_GET_PID: + { + pid_t *r_pid = va_arg (arg_ptr, pid_t *); + + if (r_pid == NULL) + return GPG_ERR_INV_VALUE; + + *r_pid = process->pid; + return 0; + } + + case GNUPG_PROCESS_GET_WSTATUS: + { + int status = process->wstatus; + int *r_if_exited = va_arg (arg_ptr, int *); + int *r_if_signaled = va_arg (arg_ptr, int *); + int *r_exit_status = va_arg (arg_ptr, int *); + int *r_termsig = va_arg (arg_ptr, int *); + + if (!process->terminated) + return GPG_ERR_UNFINISHED; + + if (WIFEXITED (status)) + { + if (r_if_exited) + *r_if_exited = 1; + if (r_if_signaled) + *r_if_signaled = 0; + if (r_exit_status) + *r_exit_status = WEXITSTATUS (status); + if (r_termsig) + *r_termsig = 0; + } + else if (WIFSIGNALED (status)) + { + if (r_if_exited) + *r_if_exited = 0; + if (r_if_signaled) + *r_if_signaled = 1; + if (r_exit_status) + *r_exit_status = 0; + if (r_termsig) + *r_termsig = WTERMSIG (status); + } + + return 0; + } + + case GNUPG_PROCESS_KILL: + { + int sig = va_arg (arg_ptr, int); + + return process_kill (process, sig); + } + + default: + break; } - xfree (dummy); - return gpg_err_make (GPG_ERR_SOURCE_DEFAULT, ec); + return GPG_ERR_UNKNOWN_COMMAND; } - - -void -gnupg_release_process (pid_t pid) +gpg_err_code_t +gnupg_process_ctl (gnupg_process_t process, unsigned int request, ...) { - (void)pid; -} + va_list arg_ptr; + gpg_err_code_t ec; + va_start (arg_ptr, request); + ec = process_vctl (process, request, arg_ptr); + va_end (arg_ptr); + return ec; +} -/* Spawn a new process and immediately detach from it. The name of - the program to exec is PGMNAME and its arguments are in ARGV (the - programname is automatically passed as first argument). - Environment strings in ENVP are set. An error is returned if - pgmname is not executable; to make this work it is necessary to - provide an absolute file name. All standard file descriptors are - connected to /dev/null. */ -gpg_error_t -gnupg_spawn_process_detached (const char *pgmname, const char *argv[], - const char *envp[] ) +gpg_err_code_t +gnupg_process_wait (gnupg_process_t process, int hang) { gpg_err_code_t ec; + int status; pid_t pid; - int i; - if (getuid() != geteuid()) - return my_error (GPG_ERR_BUG); + if (process->terminated) + /* Already terminated. */ + return 0; - if ((ec = gnupg_access (pgmname, X_OK))) - return gpg_err_make (default_errsource, ec); + pre_syscall (); + while ((pid = waitpid (process->pid, &status, hang? 0: WNOHANG)) + == (pid_t)(-1) && errno == EINTR); + post_syscall (); - pid = fork (); if (pid == (pid_t)(-1)) { - log_error (_("error forking process: %s\n"), strerror (errno)); - return my_error_from_syserror (); + ec = gpg_err_code_from_syserror (); + log_error (_("waiting for process %d to terminate failed: %s\n"), + (int)pid, gpg_strerror (ec)); } - if (!pid) + else if (!pid) { - pid_t pid2; - - gcry_control (GCRYCTL_TERM_SECMEM); - if (setsid() == -1 || chdir ("/")) - _exit (1); - - pid2 = fork (); /* Double fork to let init take over the new child. */ - if (pid2 == (pid_t)(-1)) - _exit (1); - if (pid2) - _exit (0); /* Let the parent exit immediately. */ + ec = GPG_ERR_TIMEOUT; /* Still running. */ + } + else + { + process->terminated = 1; + process->wstatus = status; + ec = 0; + } - if (envp) - for (i=0; envp[i]; i++) - putenv (xstrdup (envp[i])); + return ec; +} - do_exec (pgmname, argv, -1, -1, -1, NULL, 0); +void +gnupg_process_release (gnupg_process_t process) +{ + if (!process) + return; - /*NOTREACHED*/ + if (process->terminated) + { + gnupg_process_terminate (process); + gnupg_process_wait (process, 1); } - if (waitpid (pid, NULL, 0) == -1) - log_error ("waitpid failed in gnupg_spawn_process_detached: %s", - strerror (errno)); - - return 0; + xfree (process); } - -/* Kill a process; that is send an appropriate signal to the process. - gnupg_wait_process must be called to actually remove the process - from the system. An invalid PID is ignored. */ -void -gnupg_kill_process (pid_t pid) +gpg_err_code_t +gnupg_process_wait_list (gnupg_process_t *process_list, int count, int hang) { - if (pid != (pid_t)(-1)) + gpg_err_code_t ec = 0; + int i; + + for (i = 0; i < count; i++) { - kill (pid, SIGTERM); + if (process_list[i]->terminated) + continue; + + ec = gnupg_process_wait (process_list[i], hang); + if (ec) + break; } + + return ec; } diff --git a/common/exechelp-w32.c b/common/exechelp-w32.c index 0034e03f2..8057cc3b4 100644 --- a/common/exechelp-w32.c +++ b/common/exechelp-w32.c @@ -1,1050 +1,1236 @@ /* exechelp-w32.c - Fork and exec helpers for W32. * Copyright (C) 2004, 2007-2009, 2010 Free Software Foundation, Inc. * Copyright (C) 2004, 2006-2012, 2014-2017 g10 Code GmbH * * This file is part of GnuPG. * * This file is free software; you can redistribute it and/or modify * it under the terms of either * * - the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at * your option) any later version. * * or * * - the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at * your option) any later version. * * or both in parallel, as here. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * SPDX-License-Identifier: (LGPL-3.0+ OR GPL-2.0+) */ #include #if !defined(HAVE_W32_SYSTEM) #error This code is only used on W32. +#else +#define _WIN32_WINNT 0x600 #endif #include #include #include #include #include #ifdef HAVE_SIGNAL_H # include #endif #include #include #ifdef WITHOUT_NPTH /* Give the Makefile a chance to build without Pth. */ #undef HAVE_NPTH #undef USE_NPTH #endif #ifdef HAVE_NPTH #include #endif #ifdef HAVE_STAT # include #endif #include "util.h" #include "i18n.h" #include "sysutils.h" +#define NEED_STRUCT_SPAWN_CB_ARG #include "exechelp.h" #include +#include /* Define to 1 do enable debugging. */ #define DEBUG_W32_SPAWN 0 /* It seems Vista doesn't grok X_OK and so fails access() tests. Previous versions interpreted X_OK as F_OK anyway, so we'll just use F_OK directly. */ #undef X_OK #define X_OK F_OK /* We assume that a HANDLE can be represented by an intptr_t which should be true for all systems (HANDLE is defined as void *). Further we assume that -1 denotes an invalid handle. */ # define fd_to_handle(a) ((HANDLE)(a)) # define handle_to_fd(a) ((intptr_t)(a)) # define pid_to_handle(a) ((HANDLE)(a)) # define handle_to_pid(a) ((int)(a)) /* Helper */ static inline gpg_error_t my_error_from_syserror (void) { return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); } static inline gpg_error_t my_error (int errcode) { return gpg_err_make (default_errsource, errcode); } /* Return the maximum number of currently allowed open file descriptors. Only useful on POSIX systems but returns a value on other systems too. */ int get_max_fds (void) { int max_fds = -1; #ifdef OPEN_MAX if (max_fds == -1) max_fds = OPEN_MAX; #endif if (max_fds == -1) max_fds = 256; /* Arbitrary limit. */ return max_fds; } /* Under Windows this is a dummy function. */ void close_all_fds (int first, int *except) { (void)first; (void)except; } /* Returns an array with all currently open file descriptors. The end * of the array is marked by -1. The caller needs to release this * array using the *standard free* and not with xfree. This allow the * use of this function right at startup even before libgcrypt has * been initialized. Returns NULL on error and sets ERRNO * accordingly. Note that fstat prints a warning to DebugView for all * invalid fds which is a bit annoying. We actually do not need this * function in real code (close_all_fds is a dummy anyway) but we keep * it for use by t-exechelp.c. */ int * get_all_open_fds (void) { int *array; size_t narray; int fd, max_fd, idx; #ifndef HAVE_STAT array = calloc (1, sizeof *array); if (array) array[0] = -1; #else /*HAVE_STAT*/ struct stat statbuf; max_fd = get_max_fds (); narray = 32; /* If you change this change also t-exechelp.c. */ array = calloc (narray, sizeof *array); if (!array) return NULL; /* Note: The list we return is ordered. */ for (idx=0, fd=0; fd < max_fd; fd++) if (!(fstat (fd, &statbuf) == -1 && errno == EBADF)) { if (idx+1 >= narray) { int *tmp; narray += (narray < 256)? 32:256; tmp = realloc (array, narray * sizeof *array); if (!tmp) { free (array); return NULL; } array = tmp; } array[idx++] = fd; } array[idx] = -1; #endif /*HAVE_STAT*/ return array; } /* Helper function to build_w32_commandline. */ static char * build_w32_commandline_copy (char *buffer, const char *string) { char *p = buffer; const char *s; if (!*string) /* Empty string. */ p = stpcpy (p, "\"\""); else if (strpbrk (string, " \t\n\v\f\"")) { /* Need to do some kind of quoting. */ p = stpcpy (p, "\""); for (s=string; *s; s++) { *p++ = *s; if (*s == '\"') *p++ = *s; } *p++ = '\"'; *p = 0; } else p = stpcpy (p, string); return p; } /* Build a command line for use with W32's CreateProcess. On success CMDLINE gets the address of a newly allocated string. */ static gpg_error_t build_w32_commandline (const char *pgmname, const char * const *argv, char **cmdline) { int i, n; const char *s; char *buf, *p; *cmdline = NULL; n = 0; s = pgmname; n += strlen (s) + 1 + 2; /* (1 space, 2 quoting */ for (; *s; s++) if (*s == '\"') n++; /* Need to double inner quotes. */ for (i=0; (s=argv[i]); i++) { n += strlen (s) + 1 + 2; /* (1 space, 2 quoting */ for (; *s; s++) if (*s == '\"') n++; /* Need to double inner quotes. */ } n++; buf = p = xtrymalloc (n); if (!buf) return my_error_from_syserror (); p = build_w32_commandline_copy (p, pgmname); for (i=0; argv[i]; i++) { *p++ = ' '; p = build_w32_commandline_copy (p, argv[i]); } *cmdline= buf; return 0; } #define INHERIT_READ 1 #define INHERIT_WRITE 2 #define INHERIT_BOTH (INHERIT_READ|INHERIT_WRITE) /* Create pipe. FLAGS indicates which ends are inheritable. */ static int create_inheritable_pipe (HANDLE filedes[2], int flags) { HANDLE r, w; SECURITY_ATTRIBUTES sec_attr; memset (&sec_attr, 0, sizeof sec_attr ); sec_attr.nLength = sizeof sec_attr; sec_attr.bInheritHandle = TRUE; if (!CreatePipe (&r, &w, &sec_attr, 0)) return -1; if ((flags & INHERIT_READ) == 0) if (! SetHandleInformation (r, HANDLE_FLAG_INHERIT, 0)) goto fail; if ((flags & INHERIT_WRITE) == 0) if (! SetHandleInformation (w, HANDLE_FLAG_INHERIT, 0)) goto fail; filedes[0] = r; filedes[1] = w; return 0; fail: log_error ("SetHandleInformation failed: %s\n", w32_strerror (-1)); CloseHandle (r); CloseHandle (w); return -1; } static HANDLE w32_open_null (int for_write) { HANDLE hfile; hfile = CreateFileW (L"nul", for_write? GENERIC_WRITE : GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (hfile == INVALID_HANDLE_VALUE) log_debug ("can't open 'nul': %s\n", w32_strerror (-1)); return hfile; } static gpg_error_t create_pipe_and_estream (int filedes[2], int flags, estream_t *r_fp, int outbound, int nonblock) { gpg_error_t err = 0; HANDLE fds[2]; es_syshd_t syshd; filedes[0] = filedes[1] = -1; err = my_error (GPG_ERR_GENERAL); if (!create_inheritable_pipe (fds, flags)) { filedes[0] = _open_osfhandle (handle_to_fd (fds[0]), O_RDONLY); if (filedes[0] == -1) { log_error ("failed to translate osfhandle %p\n", fds[0]); CloseHandle (fds[1]); } else { filedes[1] = _open_osfhandle (handle_to_fd (fds[1]), O_APPEND); if (filedes[1] == -1) { log_error ("failed to translate osfhandle %p\n", fds[1]); close (filedes[0]); filedes[0] = -1; CloseHandle (fds[1]); } else err = 0; } } if (! err && r_fp) { syshd.type = ES_SYSHD_HANDLE; if (!outbound) { syshd.u.handle = fds[0]; *r_fp = es_sysopen (&syshd, nonblock? "r,nonblock" : "r"); } else { syshd.u.handle = fds[1]; *r_fp = es_sysopen (&syshd, nonblock? "w,nonblock" : "w"); } if (!*r_fp) { err = my_error_from_syserror (); log_error (_("error creating a stream for a pipe: %s\n"), gpg_strerror (err)); close (filedes[0]); close (filedes[1]); filedes[0] = filedes[1] = -1; return err; } } return err; } /* Portable function to create a pipe. Under Windows the write end is inheritable. If R_FP is not NULL, an estream is created for the read end and stored at R_FP. */ gpg_error_t gnupg_create_inbound_pipe (int filedes[2], estream_t *r_fp, int nonblock) { return create_pipe_and_estream (filedes, INHERIT_WRITE, r_fp, 0, nonblock); } /* Portable function to create a pipe. Under Windows the read end is inheritable. If R_FP is not NULL, an estream is created for the write end and stored at R_FP. */ gpg_error_t gnupg_create_outbound_pipe (int filedes[2], estream_t *r_fp, int nonblock) { return create_pipe_and_estream (filedes, INHERIT_READ, r_fp, 1, nonblock); } /* Portable function to create a pipe. Under Windows both ends are inheritable. */ gpg_error_t gnupg_create_pipe (int filedes[2]) { return create_pipe_and_estream (filedes, INHERIT_BOTH, NULL, 0, 0); } /* Close the end of a pipe. */ void gnupg_close_pipe (int fd) { if (fd != -1) close (fd); } + +struct gnupg_process { + const char *pgmname; + unsigned int terminated :1; /* or detached */ + unsigned int flags; + HANDLE hProcess; + HANDLE hd_in; + HANDLE hd_out; + HANDLE hd_err; + int exitcode; +}; + +static int gnupg_process_syscall_func_initialized; + +/* Functions called before and after blocking syscalls. */ +static void (*pre_syscall_func) (void); +static void (*post_syscall_func) (void); + +static void +check_syscall_func (void) +{ + if (!gnupg_process_syscall_func_initialized) + { + gpgrt_get_syscall_clamp (&pre_syscall_func, &post_syscall_func); + gnupg_process_syscall_func_initialized = 1; + } +} +static void +pre_syscall (void) +{ + if (pre_syscall_func) + pre_syscall_func (); +} -/* Fork and exec the PGMNAME, see exechelp.h for details. */ -gpg_error_t -gnupg_spawn_process (const char *pgmname, const char *argv[], - int *except, unsigned int flags, - estream_t *r_infp, - estream_t *r_outfp, - estream_t *r_errfp, - pid_t *pid) +static void +post_syscall (void) { - gpg_error_t err; - SECURITY_ATTRIBUTES sec_attr; - PROCESS_INFORMATION pi = - { - NULL, /* Returns process handle. */ - 0, /* Returns primary thread handle. */ - 0, /* Returns pid. */ - 0 /* Returns tid. */ - }; - STARTUPINFOW si; - int cr_flags; - char *cmdline; - wchar_t *wcmdline = NULL; - wchar_t *wpgmname = NULL; - HANDLE inpipe[2] = {INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE}; - HANDLE outpipe[2] = {INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE}; - HANDLE errpipe[2] = {INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE}; - estream_t infp = NULL; - estream_t outfp = NULL; - estream_t errfp = NULL; - HANDLE nullhd[3] = {INVALID_HANDLE_VALUE, - INVALID_HANDLE_VALUE, - INVALID_HANDLE_VALUE}; - int i, rc; - es_syshd_t syshd; - gpg_err_source_t errsource = default_errsource; - int nonblock = !!(flags & GNUPG_SPAWN_NONBLOCK); + if (post_syscall_func) + post_syscall_func (); +} - (void)except; /* Not yet used. */ - if (r_infp) - *r_infp = NULL; - if (r_outfp) - *r_outfp = NULL; - if (r_errfp) - *r_errfp = NULL; - *pid = (pid_t)(-1); /* Always required. */ +/* + * Check if STARTUPINFOEXW supports PROC_THREAD_ATTRIBUTE_HANDLE_LIST. + */ +static int +check_windows_version (void) +{ + static int is_vista_or_later = -1; + + OSVERSIONINFO osvi; - if (r_infp) + if (is_vista_or_later == -1) { - if (create_inheritable_pipe (inpipe, INHERIT_READ)) - { - err = gpg_err_make (errsource, GPG_ERR_GENERAL); - log_error (_("error creating a pipe: %s\n"), gpg_strerror (err)); - return err; - } + memset (&osvi,0,sizeof(osvi)); + osvi.dwOSVersionInfoSize = sizeof(osvi); + GetVersionEx (&osvi); - syshd.type = ES_SYSHD_HANDLE; - syshd.u.handle = inpipe[1]; - infp = es_sysopen (&syshd, nonblock? "w,nonblock" : "w"); - if (!infp) - { - err = gpg_err_make (errsource, gpg_err_code_from_syserror ()); - log_error (_("error creating a stream for a pipe: %s\n"), - gpg_strerror (err)); - CloseHandle (inpipe[0]); - CloseHandle (inpipe[1]); - inpipe[0] = inpipe[1] = INVALID_HANDLE_VALUE; - return err; - } + /* The feature is available on Vista or later. */ + is_vista_or_later = (osvi.dwMajorVersion >= 6); } - if (r_outfp) - { - if (create_inheritable_pipe (outpipe, INHERIT_WRITE)) - { - err = gpg_err_make (errsource, GPG_ERR_GENERAL); - log_error (_("error creating a pipe: %s\n"), gpg_strerror (err)); - return err; - } + return is_vista_or_later; +} - syshd.type = ES_SYSHD_HANDLE; - syshd.u.handle = outpipe[0]; - outfp = es_sysopen (&syshd, nonblock? "r,nonblock" : "r"); - if (!outfp) - { - err = gpg_err_make (errsource, gpg_err_code_from_syserror ()); - log_error (_("error creating a stream for a pipe: %s\n"), - gpg_strerror (err)); - CloseHandle (outpipe[0]); - CloseHandle (outpipe[1]); - outpipe[0] = outpipe[1] = INVALID_HANDLE_VALUE; - if (infp) - es_fclose (infp); - else if (inpipe[1] != INVALID_HANDLE_VALUE) - CloseHandle (inpipe[1]); - if (inpipe[0] != INVALID_HANDLE_VALUE) - CloseHandle (inpipe[0]); - return err; - } + +static gpg_err_code_t +spawn_detached (const char *pgmname, char *cmdline, + void (*spawn_cb) (struct spawn_cb_arg *), void *spawn_cb_arg) +{ + SECURITY_ATTRIBUTES sec_attr; + PROCESS_INFORMATION pi = { NULL, 0, 0, 0 }; + STARTUPINFOEXW si; + int cr_flags; + wchar_t *wcmdline = NULL; + wchar_t *wpgmname = NULL; + gpg_err_code_t ec; + int ret; + struct spawn_cb_arg sca; + BOOL ask_inherit = FALSE; + + ec = gnupg_access (pgmname, X_OK); + if (ec) + { + xfree (cmdline); + return ec; } - if (r_errfp) + memset (&si, 0, sizeof si); + + sca.allow_foreground_window = FALSE; + sca.hd[0] = INVALID_HANDLE_VALUE; + sca.hd[1] = INVALID_HANDLE_VALUE; + sca.hd[2] = INVALID_HANDLE_VALUE; + sca.inherit_hds = NULL; + sca.arg = spawn_cb_arg; + if (spawn_cb) + (*spawn_cb) (&sca); + + if (sca.inherit_hds) { - if (create_inheritable_pipe (errpipe, INHERIT_WRITE)) - { - err = gpg_err_make (errsource, GPG_ERR_GENERAL); - log_error (_("error creating a pipe: %s\n"), gpg_strerror (err)); - return err; - } + SIZE_T attr_list_size = 0; + HANDLE hd[16]; + HANDLE *hd_p = sca.inherit_hds; + int j = 0; - syshd.type = ES_SYSHD_HANDLE; - syshd.u.handle = errpipe[0]; - errfp = es_sysopen (&syshd, nonblock? "r,nonblock" : "r"); - if (!errfp) + if (hd_p) { - err = gpg_err_make (errsource, gpg_err_code_from_syserror ()); - log_error (_("error creating a stream for a pipe: %s\n"), - gpg_strerror (err)); - CloseHandle (errpipe[0]); - CloseHandle (errpipe[1]); - errpipe[0] = errpipe[1] = INVALID_HANDLE_VALUE; - if (outfp) - es_fclose (outfp); - else if (outpipe[0] != INVALID_HANDLE_VALUE) - CloseHandle (outpipe[0]); - if (outpipe[1] != INVALID_HANDLE_VALUE) - CloseHandle (outpipe[1]); - if (infp) - es_fclose (infp); - else if (inpipe[1] != INVALID_HANDLE_VALUE) - CloseHandle (inpipe[1]); - if (inpipe[0] != INVALID_HANDLE_VALUE) - CloseHandle (inpipe[0]); - return err; + while (*hd_p != INVALID_HANDLE_VALUE) + if (j < DIM (hd)) + hd[j++] = *hd_p++; + else + { + log_error ("Too much handles\n"); + break; + } } + + if (j) + { + if (check_windows_version ()) + { + InitializeProcThreadAttributeList (NULL, 1, 0, &attr_list_size); + si.lpAttributeList = xtrymalloc (attr_list_size); + if (si.lpAttributeList == NULL) + { + xfree (cmdline); + return gpg_err_code_from_syserror (); + } + InitializeProcThreadAttributeList (si.lpAttributeList, 1, 0, + &attr_list_size); + UpdateProcThreadAttribute (si.lpAttributeList, 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + hd, sizeof (HANDLE) * j, NULL, NULL); + } + + ask_inherit = TRUE; + } } /* Prepare security attributes. */ memset (&sec_attr, 0, sizeof sec_attr ); sec_attr.nLength = sizeof sec_attr; sec_attr.bInheritHandle = FALSE; - /* Build the command line. */ - err = build_w32_commandline (pgmname, argv, &cmdline); - if (err) - return err; - - if (inpipe[0] == INVALID_HANDLE_VALUE) - nullhd[0] = ((flags & GNUPG_SPAWN_KEEP_STDIN)? - GetStdHandle (STD_INPUT_HANDLE) : w32_open_null (0)); - if (outpipe[1] == INVALID_HANDLE_VALUE) - nullhd[1] = ((flags & GNUPG_SPAWN_KEEP_STDOUT)? - GetStdHandle (STD_OUTPUT_HANDLE) : w32_open_null (1)); - if (errpipe[1] == INVALID_HANDLE_VALUE) - nullhd[2] = ((flags & GNUPG_SPAWN_KEEP_STDOUT)? - GetStdHandle (STD_ERROR_HANDLE) : w32_open_null (1)); - - memset (&si, 0, sizeof si); - si.cb = sizeof (si); - si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; - si.wShowWindow = DEBUG_W32_SPAWN? SW_SHOW : SW_HIDE; - si.hStdInput = inpipe[0] == INVALID_HANDLE_VALUE? nullhd[0] : inpipe[0]; - si.hStdOutput = outpipe[1] == INVALID_HANDLE_VALUE? nullhd[1] : outpipe[1]; - si.hStdError = errpipe[1] == INVALID_HANDLE_VALUE? nullhd[2] : errpipe[1]; + /* Start the process. */ + si.StartupInfo.cb = sizeof (si); + si.StartupInfo.dwFlags = STARTF_USESHOWWINDOW; + si.StartupInfo.wShowWindow = DEBUG_W32_SPAWN? SW_SHOW : SW_MINIMIZE; cr_flags = (CREATE_DEFAULT_ERROR_MODE - | ((flags & GNUPG_SPAWN_DETACHED)? DETACHED_PROCESS : 0) | GetPriorityClass (GetCurrentProcess ()) - | CREATE_SUSPENDED); - /* log_debug ("CreateProcess, path='%s' cmdline='%s'\n", */ - /* pgmname, cmdline); */ + | CREATE_NEW_PROCESS_GROUP + | DETACHED_PROCESS); + /* Take care: CreateProcessW may modify wpgmname */ if (!(wpgmname = utf8_to_wchar (pgmname))) - rc = 0; + ret = 0; else if (!(wcmdline = utf8_to_wchar (cmdline))) - rc = 0; + ret = 0; else - rc = CreateProcessW (wpgmname, /* Program to start. */ - wcmdline, /* Command line arguments. */ - &sec_attr, /* Process security attributes. */ - &sec_attr, /* Thread security attributes. */ - TRUE, /* Inherit handles. */ - cr_flags, /* Creation flags. */ - NULL, /* Environment. */ - NULL, /* Use current drive/directory. */ - &si, /* Startup information. */ - &pi /* Returns process information. */ - ); - if (!rc) + ret = CreateProcessW (wpgmname, /* Program to start. */ + wcmdline, /* Command line arguments. */ + &sec_attr, /* Process security attributes. */ + &sec_attr, /* Thread security attributes. */ + ask_inherit, /* Inherit handles. */ + cr_flags, /* Creation flags. */ + NULL, /* Environment. */ + NULL, /* Use current drive/directory. */ + (STARTUPINFOW *)&si, /* Startup information. */ + &pi /* Returns process information. */ + ); + if (!ret) { if (!wpgmname || !wcmdline) log_error ("CreateProcess failed (utf8_to_wchar): %s\n", strerror (errno)); else - log_error ("CreateProcess failed: %s\n", w32_strerror (-1)); + log_error ("CreateProcess(detached) failed: %d\n", + (int)GetLastError ()); xfree (wpgmname); xfree (wcmdline); xfree (cmdline); - if (infp) - es_fclose (infp); - else if (inpipe[1] != INVALID_HANDLE_VALUE) - CloseHandle (outpipe[1]); - if (inpipe[0] != INVALID_HANDLE_VALUE) - CloseHandle (inpipe[0]); - if (outfp) - es_fclose (outfp); - else if (outpipe[0] != INVALID_HANDLE_VALUE) - CloseHandle (outpipe[0]); - if (outpipe[1] != INVALID_HANDLE_VALUE) - CloseHandle (outpipe[1]); - if (errfp) - es_fclose (errfp); - else if (errpipe[0] != INVALID_HANDLE_VALUE) - CloseHandle (errpipe[0]); - if (errpipe[1] != INVALID_HANDLE_VALUE) - CloseHandle (errpipe[1]); - return gpg_err_make (errsource, GPG_ERR_GENERAL); + return GPG_ERR_GENERAL; } + if (si.lpAttributeList) + DeleteProcThreadAttributeList (si.lpAttributeList); xfree (wpgmname); xfree (wcmdline); xfree (cmdline); - cmdline = NULL; - - /* Close the inherited handles to /dev/null. */ - for (i=0; i < DIM (nullhd); i++) - if (nullhd[i] != INVALID_HANDLE_VALUE) - CloseHandle (nullhd[i]); - /* Close the inherited ends of the pipes. */ - if (inpipe[0] != INVALID_HANDLE_VALUE) - CloseHandle (inpipe[0]); - if (outpipe[1] != INVALID_HANDLE_VALUE) - CloseHandle (outpipe[1]); - if (errpipe[1] != INVALID_HANDLE_VALUE) - CloseHandle (errpipe[1]); + /* log_debug ("CreateProcess(detached) ready: hProcess=%p hThread=%p" */ + /* " dwProcessID=%d dwThreadId=%d\n", */ + /* pi.hProcess, pi.hThread, */ + /* (int) pi.dwProcessId, (int) pi.dwThreadId); */ - /* log_debug ("CreateProcess ready: hProcess=%p hThread=%p" */ - /* " dwProcessID=%d dwThreadId=%d\n", */ - /* pi.hProcess, pi.hThread, */ - /* (int) pi.dwProcessId, (int) pi.dwThreadId); */ - /* log_debug (" outfp=%p errfp=%p\n", outfp, errfp); */ + /* Note: AllowSetForegroundWindow doesn't make sense for background + process. */ - /* Fixme: For unknown reasons AllowSetForegroundWindow returns an - invalid argument error if we pass it the correct processID. As a - workaround we use -1 (ASFW_ANY). */ - if ((flags & GNUPG_SPAWN_RUN_ASFW)) - gnupg_allow_set_foregound_window ((pid_t)(-1)/*pi.dwProcessId*/); - - /* Process has been created suspended; resume it now. */ - ResumeThread (pi.hThread); CloseHandle (pi.hThread); - - if (r_infp) - *r_infp = infp; - if (r_outfp) - *r_outfp = outfp; - if (r_errfp) - *r_errfp = errfp; - - *pid = handle_to_pid (pi.hProcess); + CloseHandle (pi.hProcess); return 0; - } +void +gnupg_spawn_helper (struct spawn_cb_arg *sca) +{ + HANDLE *user_except = sca->arg; + sca->inherit_hds = user_except; +} -/* Simplified version of gnupg_spawn_process. This function forks and - then execs PGMNAME, while connecting INFD to stdin, OUTFD to stdout - and ERRFD to stderr (any of them may be -1 to connect them to - /dev/null). The arguments for the process are expected in the NULL - terminated array ARGV. The program name itself should not be - included there. Calling gnupg_wait_process is required. - - Returns 0 on success or an error code. */ -gpg_error_t -gnupg_spawn_process_fd (const char *pgmname, const char *argv[], - int infd, int outfd, int errfd, pid_t *pid) +gpg_err_code_t +gnupg_process_spawn (const char *pgmname, const char *argv[], + unsigned int flags, + void (*spawn_cb) (struct spawn_cb_arg *), + void *spawn_cb_arg, + gnupg_process_t *r_process) { - gpg_error_t err; + gpg_err_code_t ec; + gnupg_process_t process; SECURITY_ATTRIBUTES sec_attr; PROCESS_INFORMATION pi = { NULL, 0, 0, 0 }; - STARTUPINFOW si; + STARTUPINFOEXW si; + int cr_flags; char *cmdline; wchar_t *wcmdline = NULL; wchar_t *wpgmname = NULL; - int i, rc; - HANDLE stdhd[3]; + int ret; + HANDLE hd_in[2]; + HANDLE hd_out[2]; + HANDLE hd_err[2]; + struct spawn_cb_arg sca; + int i; + BOOL ask_inherit = FALSE; + + check_syscall_func (); + + /* Build the command line. */ + ec = build_w32_commandline (pgmname, argv, &cmdline); + if (ec) + return ec; + + if ((flags & GNUPG_PROCESS_DETACHED)) + { + if ((flags & GNUPG_PROCESS_STDFDS_SETTING)) + { + xfree (cmdline); + return GPG_ERR_INV_FLAG; + } + + /* In detached case, it must be no R_PROCESS. */ + if (r_process) + { + xfree (cmdline); + return GPG_ERR_INV_ARG; + } + + return spawn_detached (pgmname, cmdline, spawn_cb, spawn_cb_arg); + } + + if (r_process) + *r_process = NULL; + + process = xtrymalloc (sizeof (struct gnupg_process)); + if (process == NULL) + { + xfree (cmdline); + return gpg_err_code_from_syserror (); + } + + process->pgmname = pgmname; + process->flags = flags; + + if ((flags & GNUPG_PROCESS_STDINOUT_SOCKETPAIR)) + { + xfree (process); + xfree (cmdline); + return GPG_ERR_NOT_SUPPORTED; + } + + if ((flags & GNUPG_PROCESS_STDIN_PIPE)) + { + ec = create_inheritable_pipe (hd_in, INHERIT_READ); + if (ec) + { + xfree (process); + xfree (cmdline); + return ec; + } + } + else if ((flags & GNUPG_PROCESS_STDIN_KEEP)) + { + hd_in[0] = GetStdHandle (STD_INPUT_HANDLE); + hd_in[1] = INVALID_HANDLE_VALUE; + } + else + { + hd_in[0] = w32_open_null (0); + hd_in[1] = INVALID_HANDLE_VALUE; + } + + if ((flags & GNUPG_PROCESS_STDOUT_PIPE)) + { + ec = create_inheritable_pipe (hd_out, INHERIT_WRITE); + if (ec) + { + if (hd_in[0] != INVALID_HANDLE_VALUE) + CloseHandle (hd_in[0]); + if (hd_in[1] != INVALID_HANDLE_VALUE) + CloseHandle (hd_in[1]); + xfree (process); + xfree (cmdline); + return ec; + } + } + else if ((flags & GNUPG_PROCESS_STDOUT_KEEP)) + { + hd_out[0] = INVALID_HANDLE_VALUE; + hd_out[1] = GetStdHandle (STD_OUTPUT_HANDLE); + } + else + { + hd_out[0] = INVALID_HANDLE_VALUE; + hd_out[1] = w32_open_null (1); + } - /* Setup return values. */ - *pid = (pid_t)(-1); + if ((flags & GNUPG_PROCESS_STDERR_PIPE)) + { + ec = create_inheritable_pipe (hd_err, INHERIT_WRITE); + if (ec) + { + if (hd_in[0] != INVALID_HANDLE_VALUE) + CloseHandle (hd_in[0]); + if (hd_in[1] != INVALID_HANDLE_VALUE) + CloseHandle (hd_in[1]); + if (hd_out[0] != INVALID_HANDLE_VALUE) + CloseHandle (hd_out[0]); + if (hd_out[1] != INVALID_HANDLE_VALUE) + CloseHandle (hd_out[1]); + xfree (process); + xfree (cmdline); + return ec; + } + } + else if ((flags & GNUPG_PROCESS_STDERR_KEEP)) + { + hd_err[0] = INVALID_HANDLE_VALUE; + hd_err[1] = GetStdHandle (STD_ERROR_HANDLE); + } + else + { + hd_err[0] = INVALID_HANDLE_VALUE; + hd_err[1] = w32_open_null (1); + } + + memset (&si, 0, sizeof si); + + sca.allow_foreground_window = FALSE; + sca.hd[0] = hd_in[0]; + sca.hd[1] = hd_out[1]; + sca.hd[2] = hd_err[1]; + sca.inherit_hds = NULL; + sca.arg = spawn_cb_arg; + if (spawn_cb) + (*spawn_cb) (&sca); + + i = 0; + if (sca.hd[0] != INVALID_HANDLE_VALUE) + i++; + if (sca.hd[1] != INVALID_HANDLE_VALUE) + i++; + if (sca.hd[2] != INVALID_HANDLE_VALUE) + i++; + + if (i != 0 || sca.inherit_hds) + { + SIZE_T attr_list_size = 0; + HANDLE hd[16]; + HANDLE *hd_p = sca.inherit_hds; + int j = 0; + + if (sca.hd[0] != INVALID_HANDLE_VALUE) + hd[j++] = sca.hd[0]; + if (sca.hd[1] != INVALID_HANDLE_VALUE) + hd[j++] = sca.hd[1]; + if (sca.hd[1] != INVALID_HANDLE_VALUE) + hd[j++] = sca.hd[2]; + if (hd_p) + { + while (*hd_p != INVALID_HANDLE_VALUE) + if (j < DIM (hd)) + hd[j++] = *hd_p++; + else + { + log_error ("Too much handles\n"); + break; + } + } + + if (j) + { + if (check_windows_version ()) + { + InitializeProcThreadAttributeList (NULL, 1, 0, &attr_list_size); + si.lpAttributeList = xtrymalloc (attr_list_size); + if (si.lpAttributeList == NULL) + { + if ((flags & GNUPG_PROCESS_STDIN_PIPE) + || !(flags & GNUPG_PROCESS_STDIN_KEEP)) + CloseHandle (hd_in[0]); + if ((flags & GNUPG_PROCESS_STDIN_PIPE)) + CloseHandle (hd_in[1]); + if ((flags & GNUPG_PROCESS_STDOUT_PIPE)) + CloseHandle (hd_out[0]); + if ((flags & GNUPG_PROCESS_STDOUT_PIPE) + || !(flags & GNUPG_PROCESS_STDOUT_KEEP)) + CloseHandle (hd_out[1]); + if ((flags & GNUPG_PROCESS_STDERR_PIPE)) + CloseHandle (hd_err[0]); + if ((flags & GNUPG_PROCESS_STDERR_PIPE) + || !(flags & GNUPG_PROCESS_STDERR_KEEP)) + CloseHandle (hd_err[1]); + xfree (process); + xfree (cmdline); + return gpg_err_code_from_syserror (); + } + InitializeProcThreadAttributeList (si.lpAttributeList, 1, 0, + &attr_list_size); + UpdateProcThreadAttribute (si.lpAttributeList, 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + hd, sizeof (HANDLE) * j, NULL, NULL); + } + ask_inherit = TRUE; + } + } /* Prepare security attributes. */ memset (&sec_attr, 0, sizeof sec_attr ); sec_attr.nLength = sizeof sec_attr; sec_attr.bInheritHandle = FALSE; - /* Build the command line. */ - err = build_w32_commandline (pgmname, argv, &cmdline); - if (err) - return err; - - memset (&si, 0, sizeof si); - si.cb = sizeof (si); - si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; - si.wShowWindow = DEBUG_W32_SPAWN? SW_SHOW : SW_MINIMIZE; - stdhd[0] = infd == -1? w32_open_null (0) : INVALID_HANDLE_VALUE; - stdhd[1] = outfd == -1? w32_open_null (1) : INVALID_HANDLE_VALUE; - stdhd[2] = errfd == -1? w32_open_null (1) : INVALID_HANDLE_VALUE; - si.hStdInput = infd == -1? stdhd[0] : (void*)_get_osfhandle (infd); - si.hStdOutput = outfd == -1? stdhd[1] : (void*)_get_osfhandle (outfd); - si.hStdError = errfd == -1? stdhd[2] : (void*)_get_osfhandle (errfd); - -/* log_debug ("CreateProcess, path='%s' cmdline='%s'\n", pgmname, cmdline); */ - /* Take care: CreateProcessW may modify wpgmname */ + /* Start the process. */ + si.StartupInfo.cb = sizeof (si); + si.StartupInfo.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; + si.StartupInfo.wShowWindow = DEBUG_W32_SPAWN? SW_SHOW : SW_HIDE; + si.StartupInfo.hStdInput = sca.hd[0]; + si.StartupInfo.hStdOutput = sca.hd[1]; + si.StartupInfo.hStdError = sca.hd[2]; + + /* log_debug ("CreateProcess, path='%s' cmdline='%s'\n", pgmname, cmdline); */ + cr_flags = (CREATE_DEFAULT_ERROR_MODE + | GetPriorityClass (GetCurrentProcess ()) + | CREATE_SUSPENDED); if (!(wpgmname = utf8_to_wchar (pgmname))) - rc = 0; + ret = 0; else if (!(wcmdline = utf8_to_wchar (cmdline))) - rc = 0; + ret = 0; else - rc = CreateProcessW (wpgmname, /* Program to start. */ - wcmdline, /* Command line arguments. */ - &sec_attr, /* Process security attributes. */ - &sec_attr, /* Thread security attributes. */ - TRUE, /* Inherit handles. */ - (CREATE_DEFAULT_ERROR_MODE - | GetPriorityClass (GetCurrentProcess ()) - | CREATE_SUSPENDED | DETACHED_PROCESS), - NULL, /* Environment. */ - NULL, /* Use current drive/directory. */ - &si, /* Startup information. */ - &pi /* Returns process information. */ - ); - if (!rc) + ret = CreateProcessW (wpgmname, /* Program to start. */ + wcmdline, /* Command line arguments. */ + &sec_attr, /* Process security attributes. */ + &sec_attr, /* Thread security attributes. */ + ask_inherit, /* Inherit handles. */ + cr_flags, /* Creation flags. */ + NULL, /* Environment. */ + NULL, /* Use current drive/directory. */ + (STARTUPINFOW *)&si, /* Startup information. */ + &pi /* Returns process information. */ + ); + if (!ret) { if (!wpgmname || !wcmdline) log_error ("CreateProcess failed (utf8_to_wchar): %s\n", - strerror (errno)); + strerror (errno)); else - log_error ("CreateProcess failed: %s\n", w32_strerror (-1)); - err = my_error (GPG_ERR_GENERAL); + log_error ("CreateProcess failed: ec=%d\n", + (int)GetLastError ()); + if ((flags & GNUPG_PROCESS_STDIN_PIPE) + || !(flags & GNUPG_PROCESS_STDIN_KEEP)) + CloseHandle (hd_in[0]); + if ((flags & GNUPG_PROCESS_STDIN_PIPE)) + CloseHandle (hd_in[1]); + if ((flags & GNUPG_PROCESS_STDOUT_PIPE)) + CloseHandle (hd_out[0]); + if ((flags & GNUPG_PROCESS_STDOUT_PIPE) + || !(flags & GNUPG_PROCESS_STDOUT_KEEP)) + CloseHandle (hd_out[1]); + if ((flags & GNUPG_PROCESS_STDERR_PIPE)) + CloseHandle (hd_err[0]); + if ((flags & GNUPG_PROCESS_STDERR_PIPE) + || !(flags & GNUPG_PROCESS_STDERR_KEEP)) + CloseHandle (hd_err[1]); + xfree (wpgmname); + xfree (wcmdline); + xfree (process); + xfree (cmdline); + return GPG_ERR_GENERAL; } - else - err = 0; + + if (si.lpAttributeList) + DeleteProcThreadAttributeList (si.lpAttributeList); xfree (wpgmname); xfree (wcmdline); xfree (cmdline); - for (i=0; i < 3; i++) - if (stdhd[i] != INVALID_HANDLE_VALUE) - CloseHandle (stdhd[i]); - if (err) - return err; -/* log_debug ("CreateProcess ready: hProcess=%p hThread=%p" */ -/* " dwProcessID=%d dwThreadId=%d\n", */ -/* pi.hProcess, pi.hThread, */ -/* (int) pi.dwProcessId, (int) pi.dwThreadId); */ + if ((flags & GNUPG_PROCESS_STDIN_PIPE) + || !(flags & GNUPG_PROCESS_STDIN_KEEP)) + CloseHandle (hd_in[0]); + if ((flags & GNUPG_PROCESS_STDOUT_PIPE) + || !(flags & GNUPG_PROCESS_STDOUT_KEEP)) + CloseHandle (hd_out[1]); + if ((flags & GNUPG_PROCESS_STDERR_PIPE) + || !(flags & GNUPG_PROCESS_STDERR_KEEP)) + CloseHandle (hd_err[1]); + + /* log_debug ("CreateProcess ready: hProcess=%p hThread=%p" */ + /* " dwProcessID=%d dwThreadId=%d\n", */ + /* pi.hProcess, pi.hThread, */ + /* (int) pi.dwProcessId, (int) pi.dwThreadId); */ + + if (sca.allow_foreground_window) + { + /* Fixme: For unknown reasons AllowSetForegroundWindow returns + * an invalid argument error if we pass it the correct + * processID. As a workaround we use -1 (ASFW_ANY). */ + if (!AllowSetForegroundWindow (ASFW_ANY /*pi.dwProcessId*/)) + log_info ("AllowSetForegroundWindow() failed: ec=%d\n", + (int)GetLastError ()); + } /* Process has been created suspended; resume it now. */ + pre_syscall (); ResumeThread (pi.hThread); CloseHandle (pi.hThread); + post_syscall (); - *pid = handle_to_pid (pi.hProcess); - return 0; + process->hProcess = pi.hProcess; + process->hd_in = hd_in[1]; + process->hd_out = hd_out[0]; + process->hd_err = hd_err[0]; + process->exitcode = -1; + process->terminated = 0; + if (r_process == NULL) + { + ec = gnupg_process_wait (process, 1); + gnupg_process_release (process); + return ec; + } + + *r_process = process; + return 0; } +gpg_err_code_t +gnupg_process_get_fds (gnupg_process_t process, unsigned int flags, + int *r_fd_in, int *r_fd_out, int *r_fd_err) +{ + (void)flags; + if (r_fd_in) + { + *r_fd_in = _open_osfhandle ((intptr_t)process->hd_in, O_APPEND); + process->hd_in = INVALID_HANDLE_VALUE; + } + if (r_fd_out) + { + *r_fd_out = _open_osfhandle ((intptr_t)process->hd_out, O_RDONLY); + process->hd_out = INVALID_HANDLE_VALUE; + } + if (r_fd_err) + { + *r_fd_err = _open_osfhandle ((intptr_t)process->hd_err, O_RDONLY); + process->hd_err = INVALID_HANDLE_VALUE; + } -/* See exechelp.h for a description. */ -gpg_error_t -gnupg_wait_process (const char *pgmname, pid_t pid, int hang, int *r_exitcode) + return 0; +} + +gpg_err_code_t +gnupg_process_get_streams (gnupg_process_t process, unsigned int flags, + estream_t *r_fp_in, estream_t *r_fp_out, + estream_t *r_fp_err) { - return gnupg_wait_processes (&pgmname, &pid, 1, hang, r_exitcode); + int nonblock = (flags & GNUPG_PROCESS_STREAM_NONBLOCK)? 1: 0; + es_syshd_t syshd; + + syshd.type = ES_SYSHD_HANDLE; + if (r_fp_in) + { + syshd.u.handle = process->hd_in; + *r_fp_in = es_sysopen (&syshd, nonblock? "w,nonblock" : "w"); + process->hd_in = INVALID_HANDLE_VALUE; + } + if (r_fp_out) + { + syshd.u.handle = process->hd_out; + *r_fp_out = es_sysopen (&syshd, nonblock? "r,nonblock" : "r"); + process->hd_out = INVALID_HANDLE_VALUE; + } + if (r_fp_err) + { + syshd.u.handle = process->hd_err; + *r_fp_err = es_sysopen (&syshd, nonblock? "r,nonblock" : "r"); + process->hd_err = INVALID_HANDLE_VALUE; + } + return 0; } -/* See exechelp.h for a description. */ -gpg_error_t -gnupg_wait_processes (const char **pgmnames, pid_t *pids, size_t count, - int hang, int *r_exitcodes) +static gpg_err_code_t +process_kill (gnupg_process_t process, unsigned int exitcode) { gpg_err_code_t ec = 0; - size_t i; - HANDLE *procs; - int code; - procs = xtrycalloc (count, sizeof *procs); - if (procs == NULL) - return my_error_from_syserror (); + pre_syscall (); + if (TerminateProcess (process->hProcess, exitcode)) + ec = gpg_err_code_from_syserror (); + post_syscall (); + return ec; +} - for (i = 0; i < count; i++) +static gpg_err_code_t +process_vctl (gnupg_process_t process, unsigned int request, va_list arg_ptr) +{ + switch (request) { - if (r_exitcodes) - r_exitcodes[i] = -1; + case GNUPG_PROCESS_NOP: + return 0; - if (pids[i] == (pid_t)(-1)) - return my_error (GPG_ERR_INV_VALUE); + case GNUPG_PROCESS_GET_PROC_ID: + { + int *r_id = va_arg (arg_ptr, int *); - procs[i] = pid_to_handle (pids[i]); - } + if (r_id == NULL) + return GPG_ERR_INV_VALUE; - /* FIXME: We should do a pth_waitpid here. However this has not yet - been implemented. A special W32 pth system call would even be - better. */ - code = WaitForMultipleObjects (count, procs, TRUE, hang? INFINITE : 0); - switch (code) - { - case WAIT_TIMEOUT: - ec = GPG_ERR_TIMEOUT; - goto leave; + *r_id = (int)GetProcessId (process->hProcess); + return 0; + } - case WAIT_FAILED: - log_error (_("waiting for processes to terminate failed: %s\n"), - w32_strerror (-1)); - ec = GPG_ERR_GENERAL; - goto leave; + case GNUPG_PROCESS_GET_EXIT_ID: + { + int *r_exit_status = va_arg (arg_ptr, int *); + unsigned long exit_code; - case WAIT_OBJECT_0: - for (i = 0; i < count; i++) - { - DWORD exc; + *r_exit_status = -1; - if (! GetExitCodeProcess (procs[i], &exc)) - { - log_error (_("error getting exit code of process %d: %s\n"), - (int) pids[i], w32_strerror (-1) ); - ec = GPG_ERR_GENERAL; - } - else if (exc) - { - if (!r_exitcodes) - log_error (_("error running '%s': exit status %d\n"), - pgmnames[i], (int)exc); - else - r_exitcodes[i] = (int)exc; - ec = GPG_ERR_GENERAL; - } - else - { - if (r_exitcodes) - r_exitcodes[i] = 0; - } - } - break; + if (!process->terminated) + return GPG_ERR_UNFINISHED; + + if (process->hProcess == INVALID_HANDLE_VALUE) + return 0; + + if (GetExitCodeProcess (process->hProcess, &exit_code) == 0) + return gpg_err_code_from_syserror (); + + *r_exit_status = (int)exit_code; + return 0; + } + + case GNUPG_PROCESS_GET_P_HANDLE: + { + HANDLE *r_hProcess = va_arg (arg_ptr, HANDLE *); + + if (r_hProcess == NULL) + return GPG_ERR_INV_VALUE; + + *r_hProcess = process->hProcess; + process->hProcess = INVALID_HANDLE_VALUE; + return 0; + } + + case GNUPG_PROCESS_GET_HANDLES: + { + HANDLE *r_hd_in = va_arg (arg_ptr, HANDLE *); + HANDLE *r_hd_out = va_arg (arg_ptr, HANDLE *); + HANDLE *r_hd_err = va_arg (arg_ptr, HANDLE *); + + if (r_hd_in) + { + *r_hd_in = process->hd_in; + process->hd_in = INVALID_HANDLE_VALUE; + } + if (r_hd_out) + { + *r_hd_out = process->hd_out; + process->hd_out = INVALID_HANDLE_VALUE; + } + if (r_hd_err) + { + *r_hd_err = process->hd_err; + process->hd_err = INVALID_HANDLE_VALUE; + } + return 0; + } + + case GNUPG_PROCESS_GET_EXIT_CODE: + { + unsigned long *r_exitcode = va_arg (arg_ptr, unsigned long *); + + if (!process->terminated) + return GPG_ERR_UNFINISHED; + + if (process->hProcess == INVALID_HANDLE_VALUE) + { + *r_exitcode = (unsigned long)-1; + return 0; + } + + if (GetExitCodeProcess (process->hProcess, r_exitcode) == 0) + return gpg_err_code_from_syserror (); + return 0; + } + + case GNUPG_PROCESS_KILL_WITH_EC: + { + unsigned int exitcode = va_arg (arg_ptr, unsigned int); + + if (process->terminated) + return 0; + + if (process->hProcess == INVALID_HANDLE_VALUE) + return 0; + + return process_kill (process, exitcode); + } default: - log_error ("WaitForMultipleObjects returned unexpected " - "code %d\n", code); - ec = GPG_ERR_GENERAL; break; } - leave: - return gpg_err_make (GPG_ERR_SOURCE_DEFAULT, ec); + return GPG_ERR_UNKNOWN_COMMAND; } - - -void -gnupg_release_process (pid_t pid) +gpg_err_code_t +gnupg_process_ctl (gnupg_process_t process, unsigned int request, ...) { - if (pid != (pid_t)INVALID_HANDLE_VALUE) - { - HANDLE process = (HANDLE)pid; + va_list arg_ptr; + gpg_err_code_t ec; - CloseHandle (process); - } + va_start (arg_ptr, request); + ec = process_vctl (process, request, arg_ptr); + va_end (arg_ptr); + return ec; } - -/* Spawn a new process and immediately detach from it. The name of - the program to exec is PGMNAME and its arguments are in ARGV (the - programname is automatically passed as first argument). - Environment strings in ENVP are set. An error is returned if - pgmname is not executable; to make this work it is necessary to - provide an absolute file name. All standard file descriptors are - connected to /dev/null. */ -gpg_error_t -gnupg_spawn_process_detached (const char *pgmname, const char *argv[], - const char *envp[] ) +gpg_err_code_t +gnupg_process_wait (gnupg_process_t process, int hang) { - gpg_error_t err; - SECURITY_ATTRIBUTES sec_attr; - PROCESS_INFORMATION pi = - { - NULL, /* Returns process handle. */ - 0, /* Returns primary thread handle. */ - 0, /* Returns pid. */ - 0 /* Returns tid. */ - }; - STARTUPINFOW si; - int cr_flags; - char *cmdline; - wchar_t *wcmdline = NULL; - wchar_t *wpgmname = NULL; - BOOL in_job = FALSE; gpg_err_code_t ec; - int rc; - int jobdebug; + int code; - /* We don't use ENVP. */ - (void)envp; + if (process->hProcess == INVALID_HANDLE_VALUE) + return 0; - cmdline = getenv ("GNUPG_EXEC_DEBUG_FLAGS"); - jobdebug = (cmdline && (atoi (cmdline) & 1)); + pre_syscall (); + code = WaitForSingleObject (process->hProcess, hang? INFINITE : 0); + post_syscall (); - if ((ec = gnupg_access (pgmname, X_OK))) - return gpg_err_make (default_errsource, ec); + switch (code) + { + case WAIT_TIMEOUT: + ec = GPG_ERR_TIMEOUT; /* Still running. */ + break; - /* Prepare security attributes. */ - memset (&sec_attr, 0, sizeof sec_attr ); - sec_attr.nLength = sizeof sec_attr; - sec_attr.bInheritHandle = FALSE; + case WAIT_FAILED: + log_error (_("waiting for process to terminate failed: ec=%d\n"), + (int)GetLastError ()); + ec = GPG_ERR_GENERAL; + break; - /* Build the command line. */ - err = build_w32_commandline (pgmname, argv, &cmdline); - if (err) - return err; + case WAIT_OBJECT_0: + process->terminated = 1; + ec = 0; + break; - /* Start the process. */ - memset (&si, 0, sizeof si); - si.cb = sizeof (si); - si.dwFlags = STARTF_USESHOWWINDOW; - si.wShowWindow = DEBUG_W32_SPAWN? SW_SHOW : SW_MINIMIZE; + default: + log_debug ("WaitForSingleObject returned unexpected code %d\n", code); + ec = GPG_ERR_GENERAL; + break; + } - cr_flags = (CREATE_DEFAULT_ERROR_MODE - | GetPriorityClass (GetCurrentProcess ()) - | CREATE_NEW_PROCESS_GROUP - | DETACHED_PROCESS); + return ec; +} - /* Check if we were spawned as part of a Job. - * In a job we need to add CREATE_BREAKAWAY_FROM_JOB - * to the cr_flags, otherwise our child processes - * are killed when we terminate. */ - if (!IsProcessInJob (GetCurrentProcess(), NULL, &in_job)) - { - log_error ("IsProcessInJob() failed: %s\n", w32_strerror (-1)); - in_job = FALSE; - } +gpg_err_code_t +gnupg_process_terminate (gnupg_process_t process) +{ + return process_kill (process, 1); +} - if (in_job) - { - /* Only try to break away from job if it is allowed, otherwise - * CreateProcess() would fail with an "Access is denied" error. */ - JOBOBJECT_EXTENDED_LIMIT_INFORMATION info; - if (!QueryInformationJobObject (NULL, JobObjectExtendedLimitInformation, - &info, sizeof info, NULL)) - { - log_error ("QueryInformationJobObject() failed: %s\n", - w32_strerror (-1)); - } - else if ((info.BasicLimitInformation.LimitFlags & - JOB_OBJECT_LIMIT_BREAKAWAY_OK)) - { - if (jobdebug) - log_debug ("Using CREATE_BREAKAWAY_FROM_JOB flag\n"); - cr_flags |= CREATE_BREAKAWAY_FROM_JOB; - } - else if ((info.BasicLimitInformation.LimitFlags & - JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK)) - { - /* The child process should automatically detach from the job. */ - if (jobdebug) - log_debug ("Not using CREATE_BREAKAWAY_FROM_JOB flag; " - "JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK is set\n"); - } - else - { - /* It seems that the child process must remain in the job. - * This is not necessarily an error, although it can cause premature - * termination of the child process when the job is closed. */ - if (jobdebug) - log_debug ("Not using CREATE_BREAKAWAY_FROM_JOB flag\n"); - } - } - else - { - if (jobdebug) - log_debug ("Process is not in a Job\n"); - } +void +gnupg_process_release (gnupg_process_t process) +{ + if (!process) + return; - /* log_debug ("CreateProcess(detached), path='%s' cmdline='%s'\n", */ - /* pgmname, cmdline); */ - /* Take care: CreateProcessW may modify wpgmname */ - if (!(wpgmname = utf8_to_wchar (pgmname))) - rc = 0; - else if (!(wcmdline = utf8_to_wchar (cmdline))) - rc = 0; - else - rc = CreateProcessW (wpgmname, /* Program to start. */ - wcmdline, /* Command line arguments. */ - &sec_attr, /* Process security attributes. */ - &sec_attr, /* Thread security attributes. */ - FALSE, /* Inherit handles. */ - cr_flags, /* Creation flags. */ - NULL, /* Environment. */ - NULL, /* Use current drive/directory. */ - &si, /* Startup information. */ - &pi /* Returns process information. */ - ); - if (!rc) + if (process->terminated) { - if (!wpgmname || !wcmdline) - log_error ("CreateProcess failed (utf8_to_wchar): %s\n", - strerror (errno)); - else - log_error ("CreateProcess(detached) failed: %s\n", w32_strerror (-1)); - xfree (wpgmname); - xfree (wcmdline); - xfree (cmdline); - return my_error (GPG_ERR_GENERAL); + gnupg_process_terminate (process); + gnupg_process_wait (process, 1); } - xfree (wpgmname); - xfree (wcmdline); - xfree (cmdline); - cmdline = NULL; - -/* log_debug ("CreateProcess(detached) ready: hProcess=%p hThread=%p" */ -/* " dwProcessID=%d dwThreadId=%d\n", */ -/* pi.hProcess, pi.hThread, */ -/* (int) pi.dwProcessId, (int) pi.dwThreadId); */ - CloseHandle (pi.hThread); - CloseHandle (pi.hProcess); - - return 0; + xfree (process); } - -/* Kill a process; that is send an appropriate signal to the process. - gnupg_wait_process must be called to actually remove the process - from the system. An invalid PID is ignored. */ -void -gnupg_kill_process (pid_t pid) +gpg_err_code_t +gnupg_process_wait_list (gnupg_process_t *process_list, int count, int hang) { - if (pid != (pid_t) INVALID_HANDLE_VALUE) + gpg_err_code_t ec = 0; + int i; + + for (i = 0; i < count; i++) { - HANDLE process = (HANDLE) pid; + if (process_list[i]->terminated) + continue; - /* Arbitrary error code. */ - TerminateProcess (process, 1); + ec = gnupg_process_wait (process_list[i], hang); + if (ec) + break; } + + return ec; } diff --git a/common/exechelp.h b/common/exechelp.h index 3343fe598..0370b23a4 100644 --- a/common/exechelp.h +++ b/common/exechelp.h @@ -1,212 +1,175 @@ /* exechelp.h - Definitions for the fork and exec helpers * Copyright (C) 2004, 2009, 2010 Free Software Foundation, Inc. * Copyright (C) 2004, 2006-2012, 2014-2017 g10 Code GmbH * * This file is part of GnuPG. * * This file is free software; you can redistribute it and/or modify * it under the terms of either * * - the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at * your option) any later version. * * or * * - the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at * your option) any later version. * * or both in parallel, as here. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * SPDX-License-Identifier: (LGPL-3.0+ OR GPL-2.0+) */ #ifndef GNUPG_COMMON_EXECHELP_H #define GNUPG_COMMON_EXECHELP_H /* Return the maximum number of currently allowed file descriptors. Only useful on POSIX systems. */ int get_max_fds (void); /* Close all file descriptors starting with descriptor FIRST. If EXCEPT is not NULL, it is expected to be a list of file descriptors which are not to close. This list shall be sorted in ascending order with its end marked by -1. */ void close_all_fds (int first, int *except); /* Returns an array with all currently open file descriptors. The end of the array is marked by -1. The caller needs to release this array using the *standard free* and not with xfree. This allow the use of this function right at startup even before libgcrypt has been initialized. Returns NULL on error and sets ERRNO accordingly. */ int *get_all_open_fds (void); /* Portable function to create a pipe. Under Windows the write end is inheritable. If R_FP is not NULL, an estream is created for the write end and stored at R_FP. */ gpg_error_t gnupg_create_inbound_pipe (int filedes[2], estream_t *r_fp, int nonblock); /* Portable function to create a pipe. Under Windows the read end is inheritable. If R_FP is not NULL, an estream is created for the write end and stored at R_FP. */ gpg_error_t gnupg_create_outbound_pipe (int filedes[2], estream_t *r_fp, int nonblock); /* Portable function to create a pipe. Under Windows both ends are inheritable. */ gpg_error_t gnupg_create_pipe (int filedes[2]); /* Close the end of a pipe. */ void gnupg_close_pipe (int fd); -#define GNUPG_SPAWN_NONBLOCK 16 -#define GNUPG_SPAWN_RUN_ASFW 64 -#define GNUPG_SPAWN_DETACHED 128 -#define GNUPG_SPAWN_KEEP_STDIN 256 -#define GNUPG_SPAWN_KEEP_STDOUT 512 -#define GNUPG_SPAWN_KEEP_STDERR 1024 - -/* Fork and exec the program PGMNAME. - - If R_INFP is NULL connect stdin of the new process to /dev/null; if - it is not NULL store the address of a pointer to a new estream - there. If R_OUTFP is NULL connect stdout of the new process to - /dev/null; if it is not NULL store the address of a pointer to a - new estream there. If R_ERRFP is NULL connect stderr of the new - process to /dev/null; if it is not NULL store the address of a - pointer to a new estream there. On success the pid of the new - process is stored at PID. On error -1 is stored at PID and if - R_OUTFP or R_ERRFP are not NULL, NULL is stored there. - - The arguments for the process are expected in the NULL terminated - array ARGV. The program name itself should not be included there. - If PREEXEC is not NULL, the given function will be called right - before the exec. - - IF EXCEPT is not NULL, it is expected to be an ordered list of file - descriptors, terminated by an entry with the value (-1). These - file descriptors won't be closed before spawning a new program. - - Returns 0 on success or an error code. Calling gnupg_wait_process - and gnupg_release_process is required if the function succeeded. - - FLAGS is a bit vector: - - GNUPG_SPAWN_NONBLOCK - If set the two output streams are created in non-blocking - mode and the input stream is switched to non-blocking mode. - This is merely a convenience feature because the caller - could do the same with gpgrt_set_nonblock. Does not yet - work for Windows. - - GNUPG_SPAWN_DETACHED - If set the process will be started as a background process. - This flag is only useful under W32 (but not W32CE) systems, - so that no new console is created and pops up a console - window when starting the server. Does not work on W32CE. - - GNUPG_SPAWN_RUN_ASFW - On W32 (but not on W32CE) run AllowSetForegroundWindow for - the child. Note that due to unknown problems this actually - allows SetForegroundWindow for all children of this process. - - GNUPG_SPAWN_KEEP_STDIN - GNUPG_SPAWN_KEEP_STDOUT - GNUPG_SPAWN_KEEP_STDERR - Do not assign /dev/null to a non-required standard file - descriptor. - - */ -gpg_error_t -gnupg_spawn_process (const char *pgmname, const char *argv[], - int *execpt, unsigned int flags, - estream_t *r_infp, - estream_t *r_outfp, - estream_t *r_errfp, - pid_t *pid); - - -/* Simplified version of gnupg_spawn_process. This function forks and - then execs PGMNAME, while connecting INFD to stdin, OUTFD to stdout - and ERRFD to stderr (any of them may be -1 to connect them to - /dev/null). The arguments for the process are expected in the NULL - terminated array ARGV. The program name itself should not be - included there. Calling gnupg_wait_process and - gnupg_release_process is required. Returns 0 on success or an - error code. */ -gpg_error_t gnupg_spawn_process_fd (const char *pgmname, - const char *argv[], - int infd, int outfd, int errfd, - pid_t *pid); - - -/* If HANG is true, waits for the process identified by PID to exit; - if HANG is false, checks whether the process has terminated. - PGMNAME should be the same as supplied to the spawn function and is - only used for diagnostics. Return values: - - 0 - The process exited successful. 0 is stored at R_EXITCODE. - - GPG_ERR_GENERAL - The process exited without success. The exit code of process - is then stored at R_EXITCODE. An exit code of -1 indicates - that the process terminated abnormally (e.g. due to a signal). - - GPG_ERR_TIMEOUT - The process is still running (returned only if HANG is false). - - GPG_ERR_INV_VALUE - An invalid PID has been specified. - - Other error codes may be returned as well. Unless otherwise noted, - -1 will be stored at R_EXITCODE. R_EXITCODE may be passed as NULL - if the exit code is not required (in that case an error message will - be printed). Note that under Windows PID is not the process id but - the handle of the process. */ -gpg_error_t gnupg_wait_process (const char *pgmname, pid_t pid, int hang, - int *r_exitcode); - -/* Like gnupg_wait_process, but for COUNT processes. */ -gpg_error_t gnupg_wait_processes (const char **pgmnames, pid_t *pids, - size_t count, int hang, int *r_exitcodes); - - -/* Kill a process; that is send an appropriate signal to the process. - gnupg_wait_process must be called to actually remove the process - from the system. An invalid PID is ignored. */ -void gnupg_kill_process (pid_t pid); - -/* Release the process identified by PID. This function is actually - only required for Windows but it does not harm to always call it. - It is a nop if PID is invalid. */ -void gnupg_release_process (pid_t pid); - - -/* Spawn a new process and immediately detach from it. The name of - the program to exec is PGMNAME and its arguments are in ARGV (the - programname is automatically passed as first argument). - Environment strings in ENVP are set. An error is returned if - pgmname is not executable; to make this work it is necessary to - provide an absolute file name. */ -gpg_error_t gnupg_spawn_process_detached (const char *pgmname, - const char *argv[], - const char *envp[] ); - +/* The opaque type for a subprocess. */ +typedef struct gnupg_process *gnupg_process_t; +#ifdef HAVE_W32_SYSTEM +struct spawn_cb_arg; +#ifdef NEED_STRUCT_SPAWN_CB_ARG +struct spawn_cb_arg { + HANDLE hd[3]; + HANDLE *inherit_hds; + BOOL allow_foreground_window; + void *arg; +}; +#endif +#else +struct spawn_cb_arg { + int fds[3]; + int *except_fds; + void *arg; +}; +#endif + +#define GNUPG_PROCESS_DETACHED (1 << 1) + +/* Specify how to keep/connect standard fds. */ +#define GNUPG_PROCESS_STDIN_PIPE (1 << 8) +#define GNUPG_PROCESS_STDOUT_PIPE (1 << 9) +#define GNUPG_PROCESS_STDERR_PIPE (1 << 10) +#define GNUPG_PROCESS_STDINOUT_SOCKETPAIR (1 << 11) +#define GNUPG_PROCESS_STDIN_KEEP (1 << 12) +#define GNUPG_PROCESS_STDOUT_KEEP (1 << 13) +#define GNUPG_PROCESS_STDERR_KEEP (1 << 14) +#define GNUPG_PROCESS_STDFDS_SETTING ( GNUPG_PROCESS_STDIN_PIPE \ + | GNUPG_PROCESS_STDOUT_PIPE | GNUPG_PROCESS_STDERR_PIPE \ + | GNUPG_PROCESS_STDINOUT_SOCKETPAIR | GNUPG_PROCESS_STDIN_KEEP \ + | GNUPG_PROCESS_STDOUT_KEEP | GNUPG_PROCESS_STDERR_KEEP) + +#define GNUPG_PROCESS_STREAM_NONBLOCK (1 << 16) + +/* Spawn helper. */ +void gnupg_spawn_helper (struct spawn_cb_arg *sca); + +/* Spawn PGMNAME. */ +gpg_err_code_t gnupg_process_spawn (const char *pgmname, const char *argv[], + unsigned int flags, + void (*spawn_cb) (struct spawn_cb_arg *), + void *spawn_cb_arg, + gnupg_process_t *r_process); + +/* Get FDs for subprocess I/O. It is the caller which should care + FDs (closing FDs). */ +gpg_err_code_t gnupg_process_get_fds (gnupg_process_t process, + unsigned int flags, + int *r_fd_in, int *r_fd_out, + int *r_fd_err); + +/* Get STREAMs for subprocess I/O. It is the caller which should care + STREAMs (closing STREAMs). */ +gpg_err_code_t gnupg_process_get_streams (gnupg_process_t process, + unsigned int flags, + gpgrt_stream_t *r_fp_in, + gpgrt_stream_t *r_fp_out, + gpgrt_stream_t *r_fp_err); + +enum gnupg_process_requests + { + /* Portable requests */ + GNUPG_PROCESS_NOP = 0, + GNUPG_PROCESS_GET_PROC_ID = 1, + GNUPG_PROCESS_GET_EXIT_ID = 2, + + /* POSIX only */ + GNUPG_PROCESS_GET_PID = 16, + GNUPG_PROCESS_GET_WSTATUS = 17, + GNUPG_PROCESS_KILL = 18, + + /* Windows only */ + GNUPG_PROCESS_GET_P_HANDLE = 32, + GNUPG_PROCESS_GET_HANDLES = 33, + GNUPG_PROCESS_GET_EXIT_CODE = 34, + GNUPG_PROCESS_KILL_WITH_EC = 35 + }; + +/* Control of a process. */ +gpg_err_code_t gnupg_process_ctl (gnupg_process_t process, + unsigned int request, ...); + +/* Wait for a single PROCESS. */ +gpg_err_code_t gnupg_process_wait (gnupg_process_t process, int hang); + +/* Terminate a PROCESS. */ +gpg_err_code_t gnupg_process_terminate (gnupg_process_t process); + +/* Release PROCESS resources. */ +void gnupg_process_release (gnupg_process_t process); + +/* Wait for a multiple processes. */ +gpg_err_code_t gnupg_process_wait_list (gnupg_process_t *process_list, + int count, int hang); #endif /*GNUPG_COMMON_EXECHELP_H*/ diff --git a/common/exectool.c b/common/exectool.c index aaf5898d0..3505c25f1 100644 --- a/common/exectool.c +++ b/common/exectool.c @@ -1,661 +1,673 @@ /* exectool.c - Utility functions to execute a helper tool * Copyright (C) 2015 Werner Koch * Copyright (C) 2016 g10 Code GmbH * * This file is part of GnuPG. * * This file is free software; you can redistribute it and/or modify * it under the terms of either * * - the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at * your option) any later version. * * or * * - the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at * your option) any later version. * * or both in parallel, as here. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include + #include "i18n.h" #include "logging.h" #include "membuf.h" #include "mischelp.h" +#ifdef HAVE_W32_SYSTEM +#define NEED_STRUCT_SPAWN_CB_ARG 1 +#endif #include "exechelp.h" #include "sysutils.h" #include "util.h" #include "exectool.h" typedef struct { const char *pgmname; exec_tool_status_cb_t status_cb; void *status_cb_value; int cont; int quiet; size_t used; size_t buffer_size; char *buffer; } read_and_log_buffer_t; static inline gpg_error_t my_error_from_syserror (void) { return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); } static void read_and_log_stderr (read_and_log_buffer_t *state, es_poll_t *fderr) { gpg_error_t err; int c; if (!fderr) { /* Flush internal buffer. */ if (state->used) { const char *pname; int len; state->buffer[state->used] = 0; state->used = 0; pname = strrchr (state->pgmname, '/'); if (pname && pname != state->pgmname && pname[1]) pname++; else pname = state->pgmname; len = strlen (pname); if (state->status_cb && !strncmp (state->buffer, "[GNUPG:] ", 9) && state->buffer[9] >= 'A' && state->buffer[9] <= 'Z') { char *rest; rest = strchr (state->buffer + 9, ' '); if (!rest) { /* Set REST to an empty string. */ rest = state->buffer + strlen (state->buffer); } else { *rest++ = 0; trim_spaces (rest); } state->status_cb (state->status_cb_value, state->buffer + 9, rest); } else if (state->quiet) ; else if (!state->cont && !strncmp (state->buffer, pname, len) && strlen (state->buffer) > strlen (pname) && state->buffer[len] == ':' ) { /* PGMNAME plus colon is identical to the start of the output: print only the output. */ log_info ("%s\n", state->buffer); } else log_info ("%s%c %s\n", pname, state->cont? '+':':', state->buffer); } state->cont = 0; return; } for (;;) { c = es_fgetc (fderr->stream); if (c == EOF) { if (es_feof (fderr->stream)) { fderr->ignore = 1; /* Not anymore needed. */ } else if (es_ferror (fderr->stream)) { err = my_error_from_syserror (); log_error ("error reading stderr of '%s': %s\n", state->pgmname, gpg_strerror (err)); fderr->ignore = 1; /* Disable. */ } break; } else if (c == '\n') { read_and_log_stderr (state, NULL); } else { if (state->used >= state->buffer_size - 1) { if (state->status_cb) { /* A status callback requires that we have a full * line. Thus we need to enlarget the buffer in * this case. */ char *newbuffer; size_t newsize = state->buffer_size + 256; newbuffer = xtrymalloc (newsize); if (!newbuffer) { log_error ("error allocating memory for status cb: %s\n", gpg_strerror (my_error_from_syserror ())); /* We better disable the status CB in this case. */ state->status_cb = NULL; read_and_log_stderr (state, NULL); state->cont = 1; } else { memcpy (newbuffer, state->buffer, state->used); xfree (state->buffer); state->buffer = newbuffer; state->buffer_size = newsize; } } else { read_and_log_stderr (state, NULL); state->cont = 1; } } state->buffer[state->used++] = c; } } } /* A buffer to copy from one stream to another. */ struct copy_buffer { char buffer[4096]; char *writep; size_t nread; }; /* Initialize a copy buffer. */ static void copy_buffer_init (struct copy_buffer *c) { c->writep = c->buffer; c->nread = 0; } /* Securely wipe a copy buffer. */ static void copy_buffer_shred (struct copy_buffer *c) { if (c == NULL) return; wipememory (c->buffer, sizeof c->buffer); c->writep = NULL; c->nread = ~0U; } /* Copy data from SOURCE to SINK using copy buffer C. */ static gpg_error_t copy_buffer_do_copy (struct copy_buffer *c, estream_t source, estream_t sink) { gpg_error_t err; size_t nwritten = 0; if (c->nread == 0) { c->writep = c->buffer; if (es_read (source, c->buffer, sizeof c->buffer, &c->nread)) { err = my_error_from_syserror (); if (gpg_err_code (err) == GPG_ERR_EAGAIN) return 0; /* We will just retry next time. */ return err; } log_assert (c->nread <= sizeof c->buffer); } if (c->nread == 0) return 0; /* Done copying. */ nwritten = 0; if (sink && es_write (sink, c->writep, c->nread, &nwritten)) err = my_error_from_syserror (); else err = 0; log_assert (nwritten <= c->nread); c->writep += nwritten; c->nread -= nwritten; log_assert (c->writep - c->buffer <= sizeof c->buffer); if (err) { if (gpg_err_code (err) == GPG_ERR_EAGAIN) return 0; /* We will just retry next time. */ return err; } if (sink && es_fflush (sink) && errno != EAGAIN) err = my_error_from_syserror (); return err; } /* Flush the remaining data to SINK. */ static gpg_error_t copy_buffer_flush (struct copy_buffer *c, estream_t sink) { gpg_error_t err = 0; size_t nwritten = 0; if (es_write (sink, c->writep, c->nread, &nwritten)) err = my_error_from_syserror (); log_assert (nwritten <= c->nread); c->writep += nwritten; c->nread -= nwritten; log_assert (c->writep - c->buffer <= sizeof c->buffer); if (err) return err; if (es_fflush (sink)) err = my_error_from_syserror (); return err; } - /* Run the program PGMNAME with the command line arguments given in * the NULL terminates array ARGV. If INPUT is not NULL it will be * fed to stdin of the process. stderr is logged using log_info and * the process's stdout is written to OUTPUT. If OUTPUT is NULL the * output is discarded. If INEXTRA is given, an additional input * stream will be passed to the child; to tell the child about this * ARGV is scanned and the first occurrence of an argument * "-&@INEXTRA@" is replaced by the concatenation of "-&" and the * child's file descriptor of the pipe created for the INEXTRA stream. * * On error a diagnostic is printed and an error code returned. */ gpg_error_t gnupg_exec_tool_stream (const char *pgmname, const char *argv[], estream_t input, estream_t inextra, estream_t output, exec_tool_status_cb_t status_cb, void *status_cb_value) { gpg_error_t err; - pid_t pid = (pid_t) -1; + gnupg_process_t proc = NULL; estream_t infp = NULL; estream_t extrafp = NULL; estream_t outfp = NULL, errfp = NULL; es_poll_t fds[4]; int exceptclose[2]; int extrapipe[2] = {-1, -1}; char extrafdbuf[20]; const char *argsave = NULL; int argsaveidx; int count; read_and_log_buffer_t fderrstate; struct copy_buffer *cpbuf_in = NULL, *cpbuf_out = NULL, *cpbuf_extra = NULL; int quiet = 0; - int dummy_exitcode; memset (fds, 0, sizeof fds); memset (&fderrstate, 0, sizeof fderrstate); /* If the first argument to the program is "--quiet" avoid all extra * diagnostics. */ quiet = (argv && argv[0] && !strcmp (argv[0], "--quiet")); cpbuf_in = xtrymalloc (sizeof *cpbuf_in); if (cpbuf_in == NULL) { err = my_error_from_syserror (); goto leave; } copy_buffer_init (cpbuf_in); cpbuf_out = xtrymalloc (sizeof *cpbuf_out); if (cpbuf_out == NULL) { err = my_error_from_syserror (); goto leave; } copy_buffer_init (cpbuf_out); cpbuf_extra = xtrymalloc (sizeof *cpbuf_extra); if (cpbuf_extra == NULL) { err = my_error_from_syserror (); goto leave; } copy_buffer_init (cpbuf_extra); fderrstate.pgmname = pgmname; fderrstate.quiet = quiet; fderrstate.status_cb = status_cb; fderrstate.status_cb_value = status_cb_value; fderrstate.buffer_size = 256; fderrstate.buffer = xtrymalloc (fderrstate.buffer_size); if (!fderrstate.buffer) { err = my_error_from_syserror (); goto leave; } if (inextra) { err = gnupg_create_outbound_pipe (extrapipe, &extrafp, 1); if (err) { log_error ("error creating outbound pipe for extra fp: %s\n", gpg_strerror (err)); goto leave; } exceptclose[0] = extrapipe[0]; /* Do not close in child. */ exceptclose[1] = -1; /* Now find the argument marker and replace by the pipe's fd. Yeah, that is an ugly non-thread safe hack but it safes us to create a copy of the array. */ #ifdef HAVE_W32_SYSTEM snprintf (extrafdbuf, sizeof extrafdbuf, "-&%lu", (unsigned long)_get_osfhandle (extrapipe[0])); #else snprintf (extrafdbuf, sizeof extrafdbuf, "-&%d", extrapipe[0]); #endif for (argsaveidx=0; argv[argsaveidx]; argsaveidx++) if (!strcmp (argv[argsaveidx], "-&@INEXTRA@")) { argsave = argv[argsaveidx]; argv[argsaveidx] = extrafdbuf; break; } } else exceptclose[0] = -1; - err = gnupg_spawn_process (pgmname, argv, - exceptclose, GNUPG_SPAWN_NONBLOCK, - input? &infp : NULL, - &outfp, &errfp, &pid); + err = gnupg_process_spawn (pgmname, argv, + ((input + ? GNUPG_PROCESS_STDIN_PIPE + : 0) + | GNUPG_PROCESS_STDOUT_PIPE + | GNUPG_PROCESS_STDERR_PIPE), + gnupg_spawn_helper, exceptclose, &proc); + gnupg_process_get_streams (proc, GNUPG_PROCESS_STREAM_NONBLOCK, + input? &infp : NULL, &outfp, &errfp); if (extrapipe[0] != -1) close (extrapipe[0]); if (argsave) argv[argsaveidx] = argsave; if (err) { if (!quiet) log_error ("error running '%s': %s\n", pgmname, gpg_strerror (err)); goto leave; } fds[0].stream = infp; fds[0].want_write = 1; if (!input) fds[0].ignore = 1; fds[1].stream = outfp; fds[1].want_read = 1; fds[2].stream = errfp; fds[2].want_read = 1; fds[3].stream = extrafp; fds[3].want_write = 1; if (!inextra) fds[3].ignore = 1; /* Now read as long as we have something to poll. We continue reading even after EOF or error on stdout so that we get the other error messages or remaining output. */ while (! (fds[1].ignore && fds[2].ignore)) { count = es_poll (fds, DIM(fds), -1); if (count == -1) { err = my_error_from_syserror (); log_error ("error polling '%s': %s\n", pgmname, gpg_strerror (err)); goto leave; } if (!count) { log_debug ("unexpected timeout while polling '%s'\n", pgmname); break; } if (fds[0].got_write) { err = copy_buffer_do_copy (cpbuf_in, input, fds[0].stream); if (err) { log_error ("error feeding data to '%s': %s\n", pgmname, gpg_strerror (err)); goto leave; } if (es_feof (input)) { err = copy_buffer_flush (cpbuf_in, fds[0].stream); if (gpg_err_code (err) == GPG_ERR_EAGAIN) continue; /* Retry next time. */ if (err) { log_error ("error feeding data to '%s': %s\n", pgmname, gpg_strerror (err)); goto leave; } fds[0].ignore = 1; /* ready. */ es_fclose (infp); infp = NULL; } } if (fds[3].got_write) { log_assert (inextra); err = copy_buffer_do_copy (cpbuf_extra, inextra, fds[3].stream); if (err) { log_error ("error feeding data to '%s': %s\n", pgmname, gpg_strerror (err)); goto leave; } if (es_feof (inextra)) { err = copy_buffer_flush (cpbuf_extra, fds[3].stream); if (gpg_err_code (err) == GPG_ERR_EAGAIN) continue; /* Retry next time. */ if (err) { log_error ("error feeding data to '%s': %s\n", pgmname, gpg_strerror (err)); goto leave; } fds[3].ignore = 1; /* ready. */ es_fclose (extrafp); extrafp = NULL; } } if (fds[1].got_read) { err = copy_buffer_do_copy (cpbuf_out, fds[1].stream, output); if (err) { log_error ("error reading data from '%s': %s\n", pgmname, gpg_strerror (err)); goto leave; } if (es_feof (fds[1].stream)) { err = copy_buffer_flush (cpbuf_out, output); if (err) { log_error ("error reading data from '%s': %s\n", pgmname, gpg_strerror (err)); goto leave; } fds[1].ignore = 1; /* ready. */ } } if (fds[2].got_read) read_and_log_stderr (&fderrstate, fds + 2); } read_and_log_stderr (&fderrstate, NULL); /* Flush. */ es_fclose (infp); infp = NULL; es_fclose (extrafp); extrafp = NULL; es_fclose (outfp); outfp = NULL; es_fclose (errfp); errfp = NULL; - err = gnupg_wait_process (pgmname, pid, 1, quiet? &dummy_exitcode : NULL); - pid = (pid_t)(-1); + err = gnupg_process_wait (proc, 1); + if (!err) + { /* To be compatible to old wait_process. */ + int status; + + gnupg_process_ctl (proc, GNUPG_PROCESS_GET_EXIT_ID, &status); + if (status) + err = gpg_error (GPG_ERR_GENERAL); + } leave: - if (err && pid != (pid_t) -1) - gnupg_kill_process (pid); + if (err && proc) + gnupg_process_terminate (proc); es_fclose (infp); es_fclose (extrafp); es_fclose (outfp); es_fclose (errfp); - if (pid != (pid_t)(-1)) - gnupg_wait_process (pgmname, pid, 1, quiet? &dummy_exitcode : NULL); - gnupg_release_process (pid); + gnupg_process_release (proc); copy_buffer_shred (cpbuf_in); xfree (cpbuf_in); copy_buffer_shred (cpbuf_out); xfree (cpbuf_out); copy_buffer_shred (cpbuf_extra); xfree (cpbuf_extra); xfree (fderrstate.buffer); return err; } /* A dummy free function to pass to 'es_mopen'. */ static void nop_free (void *ptr) { (void) ptr; } /* Run the program PGMNAME with the command line arguments given in the NULL terminates array ARGV. If INPUT_STRING is not NULL it will be fed to stdin of the process. stderr is logged using log_info and the process's stdout is returned in a newly malloced buffer RESULT with the length stored at RESULTLEN if not given as NULL. A hidden Nul is appended to the output. On error NULL is stored at RESULT, a diagnostic is printed, and an error code returned. */ gpg_error_t gnupg_exec_tool (const char *pgmname, const char *argv[], const char *input_string, char **result, size_t *resultlen) { gpg_error_t err; estream_t input = NULL; estream_t output; size_t len; size_t nread; *result = NULL; if (resultlen) *resultlen = 0; if (input_string) { len = strlen (input_string); input = es_mopen ((char *) input_string, len, len, 0 /* don't grow */, NULL, nop_free, "rb"); if (! input) return my_error_from_syserror (); } output = es_fopenmem (0, "wb"); if (! output) { err = my_error_from_syserror (); goto leave; } err = gnupg_exec_tool_stream (pgmname, argv, input, NULL, output, NULL, NULL); if (err) goto leave; len = es_ftello (output); err = es_fseek (output, 0, SEEK_SET); if (err) goto leave; *result = xtrymalloc (len + 1); if (!*result) { err = my_error_from_syserror (); goto leave; } if (len) { if (es_read (output, *result, len, &nread)) { err = my_error_from_syserror (); goto leave; } if (nread != len) log_fatal ("%s: short read from memstream\n", __func__); } (*result)[len] = 0; if (resultlen) *resultlen = len; leave: es_fclose (input); es_fclose (output); if (err) { xfree (*result); *result = NULL; } return err; } diff --git a/dirmngr/ldap-wrapper.c b/dirmngr/ldap-wrapper.c index 23d514cf9..2ec944c72 100644 --- a/dirmngr/ldap-wrapper.c +++ b/dirmngr/ldap-wrapper.c @@ -1,936 +1,933 @@ /* ldap-wrapper.c - LDAP access via a wrapper process * Copyright (C) 2004, 2005, 2007, 2008, 2018 g10 Code GmbH * Copyright (C) 2010 Free Software Foundation, Inc. * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ /* * We can't use LDAP directly for these reasons: * * 1. The LDAP library is linked to separate crypto library like * OpenSSL and even if it is linked to the library we use in dirmngr * (ntbtls or gnutls) it is sometimes a different version of that * library with all the surprising failures you may get due to this. * * 2. It is huge library in particular if TLS comes into play. So * problems with unfreed memory might turn up and we don't want * this in a long running daemon. * * 3. There is no easy way for timeouts. In particular the timeout * value does not work for DNS lookups (well, this is usual) and it * seems not to work while loading a large attribute like a * CRL. Having a separate process allows us to either tell the * process to commit suicide or have our own housekepping function * kill it after some time. The latter also allows proper * cancellation of a query at any point of time. * * 4. Given that we are going out to the network and usually get back * a long response, the fork/exec overhead is acceptable. * * Note that under WindowsCE the number of processes is strongly * limited (32 processes including the kernel processes) and thus we * don't use the process approach but implement a different wrapper in * ldap-wrapper-ce.c. */ #include #include #include #include #include #include #include #include #include #include "dirmngr.h" #include "../common/exechelp.h" #include "misc.h" #include "ldap-wrapper.h" #ifdef HAVE_W32_SYSTEM #define setenv(a,b,c) SetEnvironmentVariable ((a),(b)) #else #define pth_close(fd) close(fd) #endif /* In case sysconf does not return a value we need to have a limit. */ #ifdef _POSIX_OPEN_MAX #define MAX_OPEN_FDS _POSIX_OPEN_MAX #else #define MAX_OPEN_FDS 20 #endif #define INACTIVITY_TIMEOUT (opt.ldaptimeout + 60*5) /* seconds */ #define TIMERTICK_INTERVAL 2 /* To keep track of the LDAP wrapper state we use this structure. */ struct wrapper_context_s { struct wrapper_context_s *next; - pid_t pid; /* The pid of the wrapper process. */ + gnupg_process_t proc;/* The wrapper process. */ int printable_pid; /* Helper to print diagnostics after the process has * been cleaned up. */ estream_t fp; /* Connected with stdout of the ldap wrapper. */ gpg_error_t fp_err; /* Set to the gpg_error of the last read error * if any. */ estream_t log_fp; /* Connected with stderr of the ldap wrapper. */ ctrl_t ctrl; /* Connection data. */ int ready; /* Internally used to mark to be removed contexts. */ ksba_reader_t reader;/* The ksba reader object or NULL. */ char *line; /* Used to print the log lines (malloced). */ size_t linesize; /* Allocated size of LINE. */ size_t linelen; /* Use size of LINE. */ time_t stamp; /* The last time we noticed ativity. */ int reaper_idx; /* Private to ldap_wrapper_thread. */ }; /* We keep a global list of spawned wrapper process. A separate * thread makes use of this list to log error messages and to watch * out for finished processes. Access to list is protected by a * mutex. The condition variable is used to wakeup the reaper * thread. */ static struct wrapper_context_s *reaper_list; static npth_mutex_t reaper_list_mutex = NPTH_MUTEX_INITIALIZER; static npth_cond_t reaper_run_cond = NPTH_COND_INITIALIZER; /* We need to know whether we are shutting down the process. */ static int shutting_down; /* Close the estream fp and set it to NULL. */ #define SAFE_CLOSE(fp) \ do { estream_t _fp = fp; es_fclose (_fp); fp = NULL; } while (0) static void lock_reaper_list (void) { if (npth_mutex_lock (&reaper_list_mutex)) log_fatal ("%s: failed to acquire mutex: %s\n", __func__, gpg_strerror (gpg_error_from_syserror ())); } static void unlock_reaper_list (void) { if (npth_mutex_unlock (&reaper_list_mutex)) log_fatal ("%s: failed to release mutex: %s\n", __func__, gpg_strerror (gpg_error_from_syserror ())); } /* Read a fixed amount of data from READER into BUFFER. */ static gpg_error_t read_buffer (ksba_reader_t reader, unsigned char *buffer, size_t count) { gpg_error_t err; size_t nread; while (count) { err = ksba_reader_read (reader, buffer, count, &nread); if (err) return err; buffer += nread; count -= nread; } return 0; } /* Release the wrapper context and kill a running wrapper process. */ static void destroy_wrapper (struct wrapper_context_s *ctx) { - if (ctx->pid != (pid_t)(-1)) + if (ctx->proc) { - gnupg_kill_process (ctx->pid); - gnupg_release_process (ctx->pid); + gnupg_process_terminate (ctx->proc); + gnupg_process_release (ctx->proc); } ksba_reader_release (ctx->reader); SAFE_CLOSE (ctx->fp); SAFE_CLOSE (ctx->log_fp); xfree (ctx->line); xfree (ctx); } /* Print the content of LINE to the log stream but make sure to only print complete lines. Using NULL for LINE will flush any pending output. LINE may be modified by this function. */ static void print_log_line (struct wrapper_context_s *ctx, char *line) { char *s; size_t n; if (!line) { if (ctx->line && ctx->linelen) { log_info ("%s\n", ctx->line); ctx->linelen = 0; } return; } while ((s = strchr (line, '\n'))) { *s = 0; if (ctx->line && ctx->linelen) { log_info ("%s", ctx->line); ctx->linelen = 0; log_printf ("%s\n", line); } else log_info ("%s\n", line); line = s + 1; } n = strlen (line); if (n) { if (ctx->linelen + n + 1 >= ctx->linesize) { char *tmp; size_t newsize; newsize = ctx->linesize + ((n + 255) & ~255) + 1; tmp = (ctx->line ? xtryrealloc (ctx->line, newsize) : xtrymalloc (newsize)); if (!tmp) { log_error (_("error printing log line: %s\n"), strerror (errno)); return; } ctx->line = tmp; ctx->linesize = newsize; } memcpy (ctx->line + ctx->linelen, line, n); ctx->linelen += n; ctx->line[ctx->linelen] = 0; } } /* Read data from the log stream. Returns true if the log stream * indicated EOF or error. */ static int read_log_data (struct wrapper_context_s *ctx) { int rc; size_t n; char line[256]; rc = es_read (ctx->log_fp, line, sizeof line - 1, &n); if (rc || !n) /* Error or EOF. */ { if (rc) { gpg_error_t err = gpg_error_from_syserror (); if (gpg_err_code (err) == GPG_ERR_EAGAIN) return 0; log_error (_("error reading log from ldap wrapper %d: %s\n"), - (int)ctx->pid, gpg_strerror (err)); + ctx->printable_pid, gpg_strerror (err)); } print_log_line (ctx, NULL); /* Flush. */ SAFE_CLOSE (ctx->log_fp); return 1; } line[n] = 0; print_log_line (ctx, line); if (ctx->stamp != (time_t)(-1)) ctx->stamp = time (NULL); return 0; } /* This function is run by a separate thread to maintain the list of wrappers and to log error messages from these wrappers. */ void * ldap_reaper_thread (void *dummy) { gpg_error_t err; struct wrapper_context_s *ctx; struct wrapper_context_s *ctx_prev; struct timespec abstime; struct timespec curtime; struct timespec timeout; int millisecs; gpgrt_poll_t *fparray = NULL; int fparraysize = 0; int count, i; int ret; time_t exptime; (void)dummy; npth_clock_gettime (&abstime); abstime.tv_sec += TIMERTICK_INTERVAL; for (;;) { int any_action = 0; /* Wait until we are needed and then setup the FPARRAY. */ /* Note: There is one unlock inside the block! */ lock_reaper_list (); { while (!reaper_list && !shutting_down) { if (npth_cond_wait (&reaper_run_cond, &reaper_list_mutex)) log_error ("ldap-reaper: waiting on condition failed: %s\n", gpg_strerror (gpg_error_from_syserror ())); } for (count = 0, ctx = reaper_list; ctx; ctx = ctx->next) if (ctx->log_fp) count++; if (count > fparraysize || !fparray) { /* Need to realloc the array. We simply discard it and * replace it by a new one. */ xfree (fparray); fparray = xtrycalloc (count? count : 1, sizeof *fparray); if (!fparray) { err = gpg_error_from_syserror (); log_error ("ldap-reaper can't allocate poll array: %s" " - waiting 1s\n", gpg_strerror (err)); /* Note: Here we unlock and continue! */ unlock_reaper_list (); gnupg_sleep (1); continue; } fparraysize = count; } for (count = 0, ctx = reaper_list; ctx; ctx = ctx->next) { if (ctx->log_fp) { log_assert (count < fparraysize); fparray[count].stream = ctx->log_fp; fparray[count].want_read = 1; fparray[count].ignore = 0; ctx->reaper_idx = count; count++; } else { ctx->reaper_idx = -1; fparray[count].ignore = 1; } } for (i=count; i < fparraysize; i++) fparray[i].ignore = 1; } unlock_reaper_list (); /* Note the one unlock inside the block. */ /* Compute the next timeout. */ npth_clock_gettime (&curtime); if (!(npth_timercmp (&curtime, &abstime, <))) { /* Inactivity is checked below. Nothing else to do. */ npth_clock_gettime (&abstime); abstime.tv_sec += TIMERTICK_INTERVAL; } npth_timersub (&abstime, &curtime, &timeout); millisecs = timeout.tv_sec * 1000; millisecs += timeout.tv_nsec / 1000000; if (millisecs < 0) millisecs = 1; if (DBG_EXTPROG) { log_debug ("ldap-reaper: next run (count=%d size=%d timeout=%d)\n", count, fparraysize, millisecs); for (count=0; count < fparraysize; count++) if (!fparray[count].ignore) log_debug ("ldap-reaper: fp[%d] stream=%p %s\n", count, fparray[count].stream, fparray[count].want_read? "want_read":""); } ret = es_poll (fparray, fparraysize, millisecs); if (ret < 0) { err = gpg_error_from_syserror (); log_error ("ldap-reaper failed to poll: %s" " - waiting 1s\n", gpg_strerror (err)); /* In case the reason for the error is a too large array, we * release it so that it will be allocated smaller in the * next round. */ xfree (fparray); fparray = NULL; fparraysize = 0; gnupg_sleep (1); continue; } if (DBG_EXTPROG) { for (count=0; count < fparraysize; count++) if (!fparray[count].ignore) log_debug ("ldap-reaper: fp[%d] stream=%p rc=%d %c%c%c%c%c%c%c\n", count, fparray[count].stream, ret, fparray[count].got_read? 'r':'-', fparray[count].got_write?'w':'-', fparray[count].got_oob? 'o':'-', fparray[count].got_rdhup?'H':'-', fparray[count].got_err? 'e':'-', fparray[count].got_hup? 'h':'-', fparray[count].got_nval? 'n':'-'); } /* All timestamps before exptime should be considered expired. */ exptime = time (NULL); if (exptime > INACTIVITY_TIMEOUT) exptime -= INACTIVITY_TIMEOUT; lock_reaper_list (); { for (ctx = reaper_list; ctx; ctx = ctx->next) { /* Check whether there is any logging to be done. We need * to check FPARRAYSIZE because it can be 0 in case * es_poll returned a timeout. */ if (fparraysize && ctx->log_fp && ctx->reaper_idx >= 0) { log_assert (ctx->reaper_idx < fparraysize); if (fparray[ctx->reaper_idx].got_read) { if (read_log_data (ctx)) { SAFE_CLOSE (ctx->log_fp); any_action = 1; } } } /* Check whether the process is still running. */ - if (ctx->pid != (pid_t)(-1)) + if (ctx->proc) { - int status; - - err = gnupg_wait_process ("[dirmngr_ldap]", ctx->pid, 0, - &status); + err = gnupg_process_wait (ctx->proc, 0); if (!err) { + int status; + + gnupg_process_ctl (ctx->proc, GNUPG_PROCESS_GET_EXIT_ID, + &status); if (DBG_EXTPROG) - log_info (_("ldap wrapper %d ready"), (int)ctx->pid); + log_info (_("ldap wrapper %d ready"), ctx->printable_pid); ctx->ready = 1; - gnupg_release_process (ctx->pid); - ctx->pid = (pid_t)(-1); + gnupg_process_release (ctx->proc); + ctx->proc = NULL; any_action = 1; - } - else if (gpg_err_code (err) == GPG_ERR_GENERAL) - { + if (status == 10) log_info (_("ldap wrapper %d ready: timeout\n"), - (int)ctx->pid); + ctx->printable_pid); else log_info (_("ldap wrapper %d ready: exitcode=%d\n"), - (int)ctx->pid, status); - ctx->ready = 1; - gnupg_release_process (ctx->pid); - ctx->pid = (pid_t)(-1); - any_action = 1; + ctx->printable_pid, status); } else if (gpg_err_code (err) != GPG_ERR_TIMEOUT) { log_error (_("waiting for ldap wrapper %d failed: %s\n"), - (int)ctx->pid, gpg_strerror (err)); + ctx->printable_pid, gpg_strerror (err)); any_action = 1; } } /* Check whether we should terminate the process. */ - if (ctx->pid != (pid_t)(-1) - && ctx->stamp != (time_t)(-1) && ctx->stamp < exptime) + if (ctx->proc && ctx->stamp != (time_t)(-1) && ctx->stamp < exptime) { - gnupg_kill_process (ctx->pid); + gnupg_process_terminate (ctx->proc); ctx->stamp = (time_t)(-1); log_info (_("ldap wrapper %d stalled - killing\n"), - (int)ctx->pid); + ctx->printable_pid); /* We need to close the log stream because the cleanup * loop waits for it. */ SAFE_CLOSE (ctx->log_fp); any_action = 1; } } /* If something has been printed to the log file or we got an * EOF from a wrapper, we now print the list of active * wrappers. */ if (any_action && DBG_EXTPROG) { log_debug ("ldap worker states:\n"); for (ctx = reaper_list; ctx; ctx = ctx->next) - log_debug (" c=%p pid=%d/%d rdr=%p logfp=%p" + log_debug (" c=%p pid=%d rdr=%p logfp=%p" " ctrl=%p/%d la=%lu rdy=%d\n", ctx, - (int)ctx->pid, (int)ctx->printable_pid, + ctx->printable_pid, ctx->reader, ctx->log_fp, ctx->ctrl, ctx->ctrl? ctx->ctrl->refcount:0, (unsigned long)ctx->stamp, ctx->ready); } /* An extra loop to check whether ready marked wrappers may be * removed. We may only do so if the ksba reader object is * not anymore in use or we are in shutdown state. */ again: for (ctx_prev=NULL, ctx=reaper_list; ctx; ctx_prev=ctx, ctx=ctx->next) { if (ctx->ready && ((!ctx->log_fp && !ctx->reader) || shutting_down)) { if (ctx_prev) ctx_prev->next = ctx->next; else reaper_list = ctx->next; destroy_wrapper (ctx); goto again; } } } unlock_reaper_list (); } /*NOTREACHED*/ return NULL; /* Make the compiler happy. */ } /* Start the reaper thread for the ldap wrapper. */ void ldap_reaper_launch_thread (void) { static int done; npth_attr_t tattr; npth_t thread; int err; if (done) return; done = 1; #ifdef HAVE_W32_SYSTEM /* Static init does not yet work in W32 nPth. */ if (npth_cond_init (&reaper_run_cond, NULL)) log_fatal ("%s: failed to init condition variable: %s\n", __func__, gpg_strerror (gpg_error_from_syserror ())); #endif npth_attr_init (&tattr); npth_attr_setdetachstate (&tattr, NPTH_CREATE_DETACHED); if (npth_create (&thread, &tattr, ldap_reaper_thread, NULL)) { err = gpg_error_from_syserror (); log_error ("error spawning ldap reaper reaper thread: %s\n", gpg_strerror (err) ); dirmngr_exit (1); } npth_setname_np (thread, "ldap-reaper"); npth_attr_destroy (&tattr); } /* Wait until all ldap wrappers have terminated. We assume that the kill has already been sent to all of them. */ void ldap_wrapper_wait_connections (void) { lock_reaper_list (); { shutting_down = 1; if (npth_cond_signal (&reaper_run_cond)) log_error ("%s: Ooops: signaling condition failed: %s\n", __func__, gpg_strerror (gpg_error_from_syserror ())); } unlock_reaper_list (); while (reaper_list) gnupg_usleep (200); } /* This function is to be used to release a context associated with the given reader object. */ void ldap_wrapper_release_context (ksba_reader_t reader) { struct wrapper_context_s *ctx; if (!reader ) return; lock_reaper_list (); { for (ctx=reaper_list; ctx; ctx=ctx->next) if (ctx->reader == reader) { if (DBG_EXTPROG) - log_debug ("releasing ldap worker c=%p pid=%d/%d rdr=%p" + log_debug ("releasing ldap worker c=%p pid=%d rdr=%p" " ctrl=%p/%d\n", ctx, - (int)ctx->pid, (int)ctx->printable_pid, + ctx->printable_pid, ctx->reader, ctx->ctrl, ctx->ctrl? ctx->ctrl->refcount:0); ctx->reader = NULL; SAFE_CLOSE (ctx->fp); if (ctx->ctrl) { ctx->ctrl->refcount--; ctx->ctrl = NULL; } if (ctx->fp_err) log_info ("%s: reading from ldap wrapper %d failed: %s\n", __func__, ctx->printable_pid, gpg_strerror (ctx->fp_err)); break; } } unlock_reaper_list (); } /* Cleanup all resources held by the connection associated with CTRL. This is used after a cancel to kill running wrappers. */ void ldap_wrapper_connection_cleanup (ctrl_t ctrl) { struct wrapper_context_s *ctx; lock_reaper_list (); { for (ctx=reaper_list; ctx; ctx=ctx->next) if (ctx->ctrl && ctx->ctrl == ctrl) { ctx->ctrl->refcount--; ctx->ctrl = NULL; - if (ctx->pid != (pid_t)(-1)) - gnupg_kill_process (ctx->pid); + if (ctx->proc) + gnupg_process_terminate (ctx->proc); if (ctx->fp_err) log_info ("%s: reading from ldap wrapper %d failed: %s\n", __func__, ctx->printable_pid, gpg_strerror (ctx->fp_err)); } } unlock_reaper_list (); } /* This is the callback used by the ldap wrapper to feed the ksba * reader with the wrapper's stdout. See the description of * ksba_reader_set_cb for details. */ static int reader_callback (void *cb_value, char *buffer, size_t count, size_t *nread) { struct wrapper_context_s *ctx = cb_value; size_t nleft = count; struct timespec abstime; struct timespec curtime; struct timespec timeout; int millisecs; gpgrt_poll_t fparray[1]; int ret; gpg_error_t err; /* FIXME: We might want to add some internal buffering because the ksba code does not do any buffering for itself (because a ksba reader may be detached from another stream to read other data and then it would be cumbersome to get back already buffered stuff). */ if (!buffer && !count && !nread) return -1; /* Rewind is not supported. */ /* If we ever encountered a read error, don't continue (we don't want to possibly overwrite the last error cause). Bail out also if the file descriptor has been closed. */ if (ctx->fp_err || !ctx->fp) { *nread = 0; return -1; } memset (fparray, 0, sizeof fparray); fparray[0].stream = ctx->fp; fparray[0].want_read = 1; npth_clock_gettime (&abstime); abstime.tv_sec += TIMERTICK_INTERVAL; while (nleft > 0) { npth_clock_gettime (&curtime); if (!(npth_timercmp (&curtime, &abstime, <))) { err = dirmngr_tick (ctx->ctrl); if (err) { ctx->fp_err = err; SAFE_CLOSE (ctx->fp); return -1; } npth_clock_gettime (&abstime); abstime.tv_sec += TIMERTICK_INTERVAL; } npth_timersub (&abstime, &curtime, &timeout); millisecs = timeout.tv_sec * 1000; millisecs += timeout.tv_nsec / 1000000; if (millisecs < 0) millisecs = 1; if (DBG_EXTPROG) { log_debug ("%s: fp[0] stream=%p %s\n", __func__, fparray[0].stream, fparray[0].want_read?"want_read":""); } ret = es_poll (fparray, DIM (fparray), millisecs); if (ret < 0) { ctx->fp_err = gpg_error_from_syserror (); log_error ("error polling stdout of ldap wrapper %d: %s\n", ctx->printable_pid, gpg_strerror (ctx->fp_err)); SAFE_CLOSE (ctx->fp); return -1; } if (DBG_EXTPROG) { log_debug ("%s: fp[0] stream=%p rc=%d %c%c%c%c%c%c%c\n", __func__, fparray[0].stream, ret, fparray[0].got_read? 'r':'-', fparray[0].got_write?'w':'-', fparray[0].got_oob? 'o':'-', fparray[0].got_rdhup?'H':'-', fparray[0].got_err? 'e':'-', fparray[0].got_hup? 'h':'-', fparray[0].got_nval? 'n':'-'); } if (!ret) { /* Timeout. Will be handled when calculating the next timeout. */ continue; } if (fparray[0].got_read) { size_t n; if (es_read (ctx->fp, buffer, nleft, &n)) { ctx->fp_err = gpg_error_from_syserror (); if (gpg_err_code (ctx->fp_err) == GPG_ERR_EAGAIN) ctx->fp_err = 0; else { log_error ("%s: error reading: %s (%d)\n", __func__, gpg_strerror (ctx->fp_err), ctx->fp_err); SAFE_CLOSE (ctx->fp); return -1; } } else if (!n) /* EOF */ { if (nleft == count) return -1; /* EOF. */ break; } nleft -= n; buffer += n; if (n > 0 && ctx->stamp != (time_t)(-1)) ctx->stamp = time (NULL); } } *nread = count - nleft; return 0; } /* Fork and exec the LDAP wrapper and return a new libksba reader object at READER. ARGV is a NULL terminated list of arguments for the wrapper. The function returns 0 on success or an error code. Special hack to avoid passing a password through the command line which is globally visible: If the first element of ARGV is "--pass" it will be removed and instead the environment variable DIRMNGR_LDAP_PASS will be set to the next value of ARGV. On modern OSes the environment is not visible to other users. For those old systems where it can't be avoided, we don't want to go into the hassle of passing the password via stdin; it's just too complicated and an LDAP password used for public directory lookups should not be that confidential. */ gpg_error_t ldap_wrapper (ctrl_t ctrl, ksba_reader_t *reader, const char *argv[]) { gpg_error_t err; - pid_t pid; + gnupg_process_t process; struct wrapper_context_s *ctx; int i; int j; const char **arg_list; const char *pgmname; estream_t outfp, errfp; /* It would be too simple to connect stderr just to our logging stream. The problem is that if we are running multi-threaded everything gets intermixed. Clearly we don't want this. So the only viable solutions are either to have another thread responsible for logging the messages or to add an option to the wrapper module to do the logging on its own. Given that we anyway need a way to reap the child process and this is best done using a general reaping thread, that thread can do the logging too. */ ldap_reaper_launch_thread (); *reader = NULL; /* Files: We need to prepare stdin and stdout. We get stderr from the function. */ if (!opt.ldap_wrapper_program || !*opt.ldap_wrapper_program) pgmname = gnupg_module_name (GNUPG_MODULE_NAME_DIRMNGR_LDAP); else pgmname = opt.ldap_wrapper_program; /* Create command line argument array. */ for (i = 0; argv[i]; i++) ; arg_list = xtrycalloc (i + 2, sizeof *arg_list); if (!arg_list) { err = gpg_error_from_syserror (); log_error (_("error allocating memory: %s\n"), strerror (errno)); return err; } for (i = j = 0; argv[i]; i++, j++) if (!i && argv[i + 1] && !strcmp (*argv, "--pass")) { arg_list[j] = "--env-pass"; setenv ("DIRMNGR_LDAP_PASS", argv[1], 1); i++; } else arg_list[j] = (char*) argv[i]; ctx = xtrycalloc (1, sizeof *ctx); if (!ctx) { err = gpg_error_from_syserror (); log_error (_("error allocating memory: %s\n"), strerror (errno)); xfree (arg_list); return err; } - err = gnupg_spawn_process (pgmname, arg_list, - NULL, GNUPG_SPAWN_NONBLOCK, - NULL, &outfp, &errfp, &pid); + err = gnupg_process_spawn (pgmname, arg_list, + (GNUPG_PROCESS_STDOUT_PIPE + | GNUPG_PROCESS_STDERR_PIPE), + NULL, NULL, &process); if (err) { - xfree (arg_list); + xfree (arg_list); xfree (ctx); log_error ("error running '%s': %s\n", pgmname, gpg_strerror (err)); return err; } + gnupg_process_get_streams (process, GNUPG_PROCESS_STREAM_NONBLOCK, + NULL, &outfp, &errfp); + gnupg_process_ctl (process, GNUPG_PROCESS_GET_PROC_ID, &ctx->printable_pid); - ctx->pid = pid; - ctx->printable_pid = (int) pid; + ctx->proc = process; ctx->fp = outfp; ctx->log_fp = errfp; ctx->ctrl = ctrl; ctrl->refcount++; ctx->stamp = time (NULL); err = ksba_reader_new (reader); if (!err) err = ksba_reader_set_cb (*reader, reader_callback, ctx); if (err) { xfree (arg_list); log_error (_("error initializing reader object: %s\n"), gpg_strerror (err)); destroy_wrapper (ctx); ksba_reader_release (*reader); *reader = NULL; return err; } /* Hook the context into our list of running wrappers. */ lock_reaper_list (); { ctx->reader = *reader; ctx->next = reaper_list; reaper_list = ctx; if (npth_cond_signal (&reaper_run_cond)) log_error ("ldap-wrapper: Ooops: signaling condition failed: %s (%d)\n", gpg_strerror (gpg_error_from_syserror ()), errno); } unlock_reaper_list (); if (DBG_EXTPROG) { log_debug ("ldap wrapper %d started (%p, %s)", - (int)ctx->pid, ctx->reader, pgmname); + ctx->printable_pid, ctx->reader, pgmname); for (i=0; arg_list[i]; i++) log_printf (" [%s]", arg_list[i]); log_printf ("\n"); } xfree (arg_list); /* Need to wait for the first byte so we are able to detect an empty output and not let the consumer see an EOF without further error indications. The CRL loading logic assumes that after return from this function, a failed search (e.g. host not found ) is indicated right away. */ { unsigned char c; err = read_buffer (*reader, &c, 1); if (err) { ldap_wrapper_release_context (*reader); ksba_reader_release (*reader); *reader = NULL; if (gpg_err_code (err) == GPG_ERR_EOF) return gpg_error (GPG_ERR_NO_DATA); else return err; } ksba_reader_unread (*reader, &c, 1); } return 0; } diff --git a/g10/photoid.c b/g10/photoid.c index 72e6acf7d..a866eb083 100644 --- a/g10/photoid.c +++ b/g10/photoid.c @@ -1,824 +1,822 @@ /* photoid.c - photo ID handling code * Copyright (C) 2001, 2002, 2005, 2006, 2008, 2011 Free Software Foundation, Inc. * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include #include #include #include #include #ifdef _WIN32 # ifdef HAVE_WINSOCK2_H # include # endif # include # ifndef VER_PLATFORM_WIN32_WINDOWS # define VER_PLATFORM_WIN32_WINDOWS 1 # endif #endif #include "gpg.h" #include "../common/util.h" #include "packet.h" #include "../common/status.h" #include "keydb.h" #include "../common/i18n.h" #include "../common/iobuf.h" #include "options.h" #include "main.h" #include "photoid.h" #include "../common/ttyio.h" #include "trustdb.h" #if defined (_WIN32) /* This is a nicer system() for windows that waits for programs to return before returning control to the caller. I hate helpful computers. */ static int w32_system (const char *command) { if (!strncmp (command, "!ShellExecute ", 14)) { SHELLEXECUTEINFOW see; wchar_t *wname; int waitms; command = command + 14; while (spacep (command)) command++; waitms = atoi (command); if (waitms < 0) waitms = 0; else if (waitms > 60*1000) waitms = 60000; while (*command && !spacep (command)) command++; while (spacep (command)) command++; wname = utf8_to_wchar (command); if (!wname) return -1; memset (&see, 0, sizeof see); see.cbSize = sizeof see; see.fMask = (SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC | SEE_MASK_FLAG_NO_UI | SEE_MASK_NO_CONSOLE); see.lpVerb = L"open"; see.lpFile = (LPCWSTR)wname; see.nShow = SW_SHOW; if (DBG_EXTPROG) log_debug ("running ShellExecuteEx(open,'%s')\n", command); if (!ShellExecuteExW (&see)) { if (DBG_EXTPROG) log_debug ("ShellExecuteEx failed: rc=%d\n", (int)GetLastError ()); xfree (wname); return -1; } if (DBG_EXTPROG) log_debug ("ShellExecuteEx succeeded (hProcess=%p,hInstApp=%d)\n", see.hProcess, (int)see.hInstApp); if (!see.hProcess) { gnupg_usleep (waitms*1000); if (DBG_EXTPROG) log_debug ("ShellExecuteEx ready (wait=%dms)\n", waitms); } else { WaitForSingleObject (see.hProcess, INFINITE); if (DBG_EXTPROG) log_debug ("ShellExecuteEx ready\n"); } CloseHandle (see.hProcess); xfree (wname); } else { char *string; wchar_t *wstring; PROCESS_INFORMATION pi; STARTUPINFOW si; /* We must use a copy of the command as CreateProcess modifies * this argument. */ string = xstrdup (command); wstring = utf8_to_wchar (string); xfree (string); if (!wstring) return -1; memset (&pi, 0, sizeof(pi)); memset (&si, 0, sizeof(si)); si.cb = sizeof (si); if (!CreateProcessW (NULL, wstring, NULL, NULL, FALSE, DETACHED_PROCESS, NULL, NULL, &si, &pi)) { xfree (wstring); return -1; } /* Wait for the child to exit */ WaitForSingleObject (pi.hProcess, INFINITE); CloseHandle (pi.hProcess); CloseHandle (pi.hThread); xfree (wstring); } return 0; } #endif /*_W32*/ /* Generate a new photo id packet, or return NULL if canceled. FIXME: Should we add a duplicates check similar to generate_user_id? */ PKT_user_id * generate_photo_id (ctrl_t ctrl, PKT_public_key *pk,const char *photo_name) { PKT_user_id *uid; int error=1,i; unsigned int len; char *filename; byte *photo=NULL; byte header[16]; IOBUF file; int overflow; header[0]=0x10; /* little side of photo header length */ header[1]=0; /* big side of photo header length */ header[2]=1; /* 1 == version of photo header */ header[3]=1; /* 1 == JPEG */ for(i=4;i<16;i++) /* The reserved bytes */ header[i]=0; #define EXTRA_UID_NAME_SPACE 71 uid=xmalloc_clear(sizeof(*uid)+71); if(photo_name && *photo_name) filename=make_filename(photo_name,(void *)NULL); else { tty_printf(_("\nPick an image to use for your photo ID." " The image must be a JPEG file.\n" "Remember that the image is stored within your public key." " If you use a\n" "very large picture, your key will become very large" " as well!\n" "Keeping the image close to 240x288 is a good size" " to use.\n")); filename=NULL; } while(photo==NULL) { if(filename==NULL) { char *tempname; tty_printf("\n"); tty_enable_completion(NULL); tempname=cpr_get("photoid.jpeg.add", _("Enter JPEG filename for photo ID: ")); tty_disable_completion(); filename=make_filename(tempname,(void *)NULL); xfree(tempname); if(strlen(filename)==0) goto scram; } file=iobuf_open(filename); if (file && is_secured_file (iobuf_get_fd (file))) { iobuf_close (file); file = NULL; gpg_err_set_errno (EPERM); } if(!file) { log_error(_("unable to open JPEG file '%s': %s\n"), filename,strerror(errno)); xfree(filename); filename=NULL; continue; } len=iobuf_get_filelength(file, &overflow); if(len>6144 || overflow) { tty_printf( _("This JPEG is really large (%d bytes) !\n"),len); if(!cpr_get_answer_is_yes("photoid.jpeg.size", _("Are you sure you want to use it? (y/N) "))) { iobuf_close(file); xfree(filename); filename=NULL; continue; } } photo=xmalloc(len); iobuf_read(file,photo,len); iobuf_close(file); /* Is it a JPEG? */ if(photo[0]!=0xFF || photo[1]!=0xD8) { log_error(_("'%s' is not a JPEG file\n"),filename); xfree(photo); photo=NULL; xfree(filename); filename=NULL; continue; } /* Build the packet */ build_attribute_subpkt(uid,1,photo,len,header,16); parse_attribute_subpkts(uid); make_attribute_uidname(uid, EXTRA_UID_NAME_SPACE); /* Showing the photo is not safe when noninteractive since the "user" may not be able to dismiss a viewer window! */ if(opt.command_fd==-1) { show_photos (ctrl, uid->attribs, uid->numattribs, pk, uid); switch(cpr_get_answer_yes_no_quit("photoid.jpeg.okay", _("Is this photo correct (y/N/q)? "))) { case -1: goto scram; case 0: free_attributes(uid); xfree(photo); photo=NULL; xfree(filename); filename=NULL; continue; } } } error=0; uid->ref=1; scram: xfree(filename); xfree(photo); if(error) { free_attributes(uid); xfree(uid); return NULL; } return uid; } /* Returns 0 for error, 1 for valid */ int parse_image_header(const struct user_attribute *attr,byte *type,u32 *len) { u16 headerlen; if(attr->len<3) return 0; /* For historical reasons (i.e. "oops!"), the header length is little endian. */ headerlen=(attr->data[1]<<8) | attr->data[0]; if(headerlen>attr->len) return 0; if(type && attr->len>=4) { if(attr->data[2]==1) /* header version 1 */ *type=attr->data[3]; else *type=0; } *len=attr->len-headerlen; if(*len==0) return 0; return 1; } /* style==0 for extension, 1 for name, 2 for MIME type. Remember that the "name" style string could be used in a user ID name field, so make sure it is not too big (see parse-packet.c:parse_attribute). Extensions should be 3 characters long for the best cross-platform compatibility. */ const char * image_type_to_string(byte type,int style) { const char *string; switch(type) { case 1: /* jpeg */ if(style==0) string="jpg"; else if(style==1) string="jpeg"; else string="image/jpeg"; break; default: if(style==0) string="bin"; else if(style==1) string="unknown"; else string="image/x-unknown"; break; } return string; } #if !defined(FIXED_PHOTO_VIEWER) && !defined(DISABLE_PHOTO_VIEWER) static const char * get_default_photo_command(void) { #if defined(_WIN32) OSVERSIONINFO osvi; memset(&osvi,0,sizeof(osvi)); osvi.dwOSVersionInfoSize=sizeof(osvi); GetVersionEx(&osvi); if(osvi.dwPlatformId==VER_PLATFORM_WIN32_WINDOWS) return "start /w %i"; else return "!ShellExecute 400 %i"; #elif defined(__APPLE__) /* OS X. This really needs more than just __APPLE__. */ return "open %I"; #else if (!path_access ("xloadimage", X_OK)) return "xloadimage -fork -quiet -title 'KeyID 0x%k' stdin"; else if (!path_access ("display",X_OK)) return "display -title 'KeyID 0x%k' %i"; else if (getuid () && !path_access ("xdg-open", X_OK)) { /* xdg-open spawns the actual program and exits so we need to * keep the temp file */ return "xdg-open %I"; } else return "/bin/true"; #endif } #endif #ifndef DISABLE_PHOTO_VIEWER struct spawn_info { unsigned int keep_temp_file; char *command; char *tempdir; char *tempfile; }; #ifdef NO_EXEC static void show_photo (const char *command, const char *name, const void *image, u32 len) { log_error(_("no remote program execution supported\n")); return GPG_ERR_GENERAL; } #else /* ! NO_EXEC */ #include "../common/membuf.h" #include "../common/exechelp.h" /* Makes a temp directory and filenames */ static int setup_input_file (struct spawn_info *info, const char *name) { char *tmp = opt.temp_dir; int len; #define TEMPLATE "gpg-XXXXXX" /* Initialize by the length of last part in the path + 1 */ len = strlen (DIRSEP_S) + strlen (TEMPLATE) + 1; /* Make up the temp dir and file in case we need them */ if (tmp) { len += strlen (tmp); info->tempdir = xmalloc (len); } else { #if defined (_WIN32) int ret; tmp = xmalloc (MAX_PATH+1); if (!tmp) return -1; ret = GetTempPath (MAX_PATH-len, tmp); if (ret == 0 || ret >= MAX_PATH-len) strcpy (tmp, "c:\\windows\\temp"); else { /* GetTempPath may return with \ on the end */ while (ret > 0 && tmp[ret-1] == '\\') { tmp[ret-1]='\0'; ret--; } } len += ret; info->tempdir = tmp; #else /* More unixish systems */ if (!(tmp = getenv ("TMPDIR")) && !(tmp = getenv ("TMP"))) tmp = "/tmp"; len += strlen (tmp); info->tempdir = xmalloc (len); #endif } if (info->tempdir == NULL) return -1; sprintf (info->tempdir, "%s" DIRSEP_S TEMPLATE, tmp); if (gnupg_mkdtemp (info->tempdir) == NULL) { log_error (_("can't create directory '%s': %s\n"), info->tempdir, strerror (errno)); return -1; } info->tempfile = xmalloc (strlen (info->tempdir) + strlen (DIRSEP_S) + strlen (name) + 1); if (info->tempfile == NULL) { xfree (info->tempdir); info->tempdir = NULL; return -1; } sprintf (info->tempfile, "%s" DIRSEP_S "%s", info->tempdir, name); return 0; } /* Expands %i or %I in the args to the full temp file within the temp directory. */ static int expand_args (struct spawn_info *info, const char *args_in, const char *name) { const char *ch = args_in; membuf_t command; info->keep_temp_file = 0; if (DBG_EXTPROG) log_debug ("expanding string \"%s\"\n", args_in); init_membuf (&command, 100); while (*ch != '\0') { if (*ch == '%') { const char *append = NULL; ch++; switch (*ch) { case 'I': info->keep_temp_file = 1; /* fall through */ case 'i': /* in */ if (info->tempfile == NULL) { if (setup_input_file (info, name) < 0) goto fail; } append = info->tempfile; break; case '%': append = "%"; break; } if (append) put_membuf_str (&command, append); } else put_membuf (&command, ch, 1); ch++; } put_membuf (&command, "", 1); /* Terminate string. */ info->command = get_membuf (&command, NULL); if (!info->command) return -1; if(DBG_EXTPROG) log_debug("args expanded to \"%s\", use %s, keep %u\n", info->command, info->tempfile, info->keep_temp_file); return 0; fail: xfree (get_membuf (&command, NULL)); return -1; } #ifndef EXEC_TEMPFILE_ONLY static void fill_command_argv (const char *argv[4], const char *command) { argv[0] = getenv ("SHELL"); if (argv[0] == NULL) argv[0] = "/bin/sh"; argv[1] = "-c"; argv[2] = command; argv[3] = NULL; } #endif static void run_with_pipe (struct spawn_info *info, const void *image, u32 len) { #ifdef EXEC_TEMPFILE_ONLY (void)info; (void)image; (void)len; log_error (_("this platform requires temporary files when calling" " external programs\n")); return; #else /* !EXEC_TEMPFILE_ONLY */ - int to[2]; - pid_t pid; gpg_error_t err; const char *argv[4]; - - err = gnupg_create_pipe (to); - if (err) - return; + gnupg_process_t proc; fill_command_argv (argv, info->command); - err = gnupg_spawn_process_fd (argv[0], argv+1, to[0], -1, -1, &pid); - - close (to[0]); - + err = gnupg_process_spawn (argv[0], argv+1, GNUPG_PROCESS_STDIN_PIPE, + NULL, NULL, &proc); if (err) - { - log_error (_("unable to execute shell '%s': %s\n"), - argv[0], gpg_strerror (err)); - close (to[1]); - } + log_error (_("unable to execute shell '%s': %s\n"), + argv[0], gpg_strerror (err)); else { - write (to[1], image, len); - close (to[1]); + int fd_in; - err = gnupg_wait_process (argv[0], pid, 1, NULL); + err = gnupg_process_get_fds (proc, 0, &fd_in, NULL, NULL); + if (err) + log_error ("unable to get pipe connection '%s': %s\n", + argv[2], gpg_strerror (err)); + else + { + write (fd_in, image, len); + close (fd_in); + } + + err = gnupg_process_wait (proc, 1); if (err) log_error (_("unnatural exit of external program\n")); + + gnupg_process_release (proc); } #endif /* !EXEC_TEMPFILE_ONLY */ } static int create_temp_file (struct spawn_info *info, const void *ptr, u32 len) { if (DBG_EXTPROG) log_debug ("using temp file '%s'\n", info->tempfile); /* It's not fork/exec/pipe, so create a temp file */ if ( is_secured_filename (info->tempfile) ) { log_error (_("can't create '%s': %s\n"), info->tempfile, strerror (EPERM)); gpg_err_set_errno (EPERM); return -1; } else { estream_t fp = es_fopen (info->tempfile, "wb"); if (fp) { es_fwrite (ptr, len, 1, fp); es_fclose (fp); return 0; } else { int save = errno; log_error (_("can't create '%s': %s\n"), info->tempfile, strerror(errno)); gpg_err_set_errno (save); return -1; } } } static void show_photo (const char *command, const char *name, const void *image, u32 len) { struct spawn_info *spawn; spawn = xmalloc_clear (sizeof (struct spawn_info)); if (!spawn) return; /* Expand the args */ if (expand_args (spawn, command, name) < 0) { xfree (spawn); return; } if (DBG_EXTPROG) log_debug ("running command: %s\n", spawn->command); if (spawn->tempfile == NULL) run_with_pipe (spawn, image, len); else if (create_temp_file (spawn, image, len) == 0) { #if defined (_WIN32) if (w32_system (spawn->command) < 0) log_error (_("system error while calling external program: %s\n"), strerror (errno)); #else - pid_t pid; gpg_error_t err; const char *argv[4]; fill_command_argv (argv, spawn->command); - err = gnupg_spawn_process_fd (argv[0], argv+1, -1, -1, -1, &pid); - if (!err) - err = gnupg_wait_process (argv[0], pid, 1, NULL); + err = gnupg_process_spawn (argv[0], argv+1, 0, NULL, NULL, NULL); if (err) log_error (_("unnatural exit of external program\n")); #endif if (!spawn->keep_temp_file) { if (unlink (spawn->tempfile) < 0) log_info (_("WARNING: unable to remove tempfile (%s) '%s': %s\n"), "in", spawn->tempfile, strerror(errno)); if (rmdir (spawn->tempdir) < 0) log_info (_("WARNING: unable to remove temp directory '%s': %s\n"), spawn->tempdir, strerror(errno)); } } xfree(spawn->command); xfree(spawn->tempdir); xfree(spawn->tempfile); xfree(spawn); } #endif #endif void show_photos (ctrl_t ctrl, const struct user_attribute *attrs, int count, PKT_public_key *pk, PKT_user_id *uid) { #ifdef DISABLE_PHOTO_VIEWER (void)attrs; (void)count; (void)pk; (void)uid; #else /*!DISABLE_PHOTO_VIEWER*/ int i; struct expando_args args; u32 len; u32 kid[2]={0,0}; if (opt.exec_disable && !opt.no_perm_warn) { log_info (_("external program calls are disabled due to unsafe " "options file permissions\n")); return; } #if defined(HAVE_GETUID) && defined(HAVE_GETEUID) /* There should be no way to get to this spot while still carrying setuid privs. Just in case, bomb out if we are. */ if ( getuid () != geteuid ()) BUG (); #endif memset (&args, 0, sizeof(args)); args.pk = pk; args.validity_info = get_validity_info (ctrl, NULL, pk, uid); args.validity_string = get_validity_string (ctrl, pk, uid); namehash_from_uid (uid); args.namehash = uid->namehash; if (pk) keyid_from_pk (pk, kid); es_fflush (es_stdout); #ifdef FIXED_PHOTO_VIEWER opt.photo_viewer = FIXED_PHOTO_VIEWER; #else if (!opt.photo_viewer) opt.photo_viewer = get_default_photo_command (); #endif for (i=0; i. */ #include #include #include #include #include #include #include #include "g13.h" #include "../common/i18n.h" #include "keyblob.h" -#include "be-encfs.h" -#include "runner.h" #include "../common/sysutils.h" #include "../common/exechelp.h" +#include "runner.h" +#include "be-encfs.h" /* Command values used to run the encfs tool. */ enum encfs_cmds { ENCFS_CMD_CREATE, ENCFS_CMD_MOUNT, ENCFS_CMD_UMOUNT }; /* An object to keep the private state of the encfs tool. It is released by encfs_handler_cleanup. */ struct encfs_parm_s { enum encfs_cmds cmd; /* The current command. */ tupledesc_t tuples; /* NULL or the tuples object. */ char *mountpoint; /* The mountpoint. */ }; typedef struct encfs_parm_s *encfs_parm_t; static gpg_error_t send_cmd_bin (runner_t runner, const void *data, size_t datalen) { return runner_send_line (runner, data, datalen); } static gpg_error_t send_cmd (runner_t runner, const char *string) { log_debug ("sending command -->%s<--\n", string); return send_cmd_bin (runner, string, strlen (string)); } static void run_umount_helper (const char *mountpoint) { gpg_error_t err; const char pgmname[] = FUSERMOUNT; const char *args[3]; args[0] = "-u"; args[1] = mountpoint; args[2] = NULL; - err = gnupg_spawn_process_detached (pgmname, args, NULL); + err = gnupg_process_spawn (pgmname, args, + GNUPG_PROCESS_DETACHED, + NULL, NULL, NULL); if (err) log_error ("failed to run '%s': %s\n", pgmname, gpg_strerror (err)); } /* Handle one line of the encfs tool's output. This function is allowed to modify the content of BUFFER. */ static gpg_error_t handle_status_line (runner_t runner, const char *line, enum encfs_cmds cmd, tupledesc_t tuples) { gpg_error_t err; /* Check that encfs understands our new options. */ if (!strncmp (line, "$STATUS$", 8)) { for (line +=8; *line && spacep (line); line++) ; log_info ("got status '%s'\n", line); if (!strcmp (line, "fuse_main_start")) { /* Send a special error code back to let the caller know that everything has been setup by encfs. */ err = gpg_error (GPG_ERR_UNFINISHED); } else err = 0; } else if (!strncmp (line, "$PROMPT$", 8)) { for (line +=8; *line && spacep (line); line++) ; log_info ("got prompt '%s'\n", line); if (!strcmp (line, "create_root_dir")) err = send_cmd (runner, cmd == ENCFS_CMD_CREATE? "y":"n"); else if (!strcmp (line, "create_mount_point")) err = send_cmd (runner, "y"); else if (!strcmp (line, "passwd") || !strcmp (line, "new_passwd")) { if (tuples) { size_t n; const void *value; value = find_tuple (tuples, KEYBLOB_TAG_ENCKEY, &n); if (!value) err = gpg_error (GPG_ERR_INV_SESSION_KEY); else if ((err = send_cmd_bin (runner, value, n))) { if (gpg_err_code (err) == GPG_ERR_BUG && gpg_err_source (err) == GPG_ERR_SOURCE_DEFAULT) err = gpg_error (GPG_ERR_INV_SESSION_KEY); } } else err = gpg_error (GPG_ERR_NO_DATA); } else err = send_cmd (runner, ""); /* Default to send an empty line. */ } else if (strstr (line, "encfs: unrecognized option '")) err = gpg_error (GPG_ERR_INV_ENGINE); else err = 0; return err; } /* The main processing function as used by the runner. */ static gpg_error_t encfs_handler (void *opaque, runner_t runner, const char *status_line) { encfs_parm_t parm = opaque; gpg_error_t err; if (!parm || !runner) return gpg_error (GPG_ERR_BUG); if (!status_line) { /* Runner requested internal flushing - nothing to do here. */ return 0; } err = handle_status_line (runner, status_line, parm->cmd, parm->tuples); if (gpg_err_code (err) == GPG_ERR_UNFINISHED && gpg_err_source (err) == GPG_ERR_SOURCE_DEFAULT) { err = 0; /* No more need for the tuples. */ destroy_tupledesc (parm->tuples); parm->tuples = NULL; if (parm->cmd == ENCFS_CMD_CREATE) { /* The encfs tool keeps on running after creation of the container. We don't want that and thus need to stop the encfs process. */ run_umount_helper (parm->mountpoint); /* In case the umount helper does not work we try to kill the engine. FIXME: We should figure out how to make fusermount work. */ runner_cancel (runner); } } return err; } /* Called by the runner to cleanup the private data. */ static void encfs_handler_cleanup (void *opaque) { encfs_parm_t parm = opaque; if (!parm) return; destroy_tupledesc (parm->tuples); xfree (parm->mountpoint); xfree (parm); } /* Run the encfs tool. */ static gpg_error_t run_encfs_tool (ctrl_t ctrl, enum encfs_cmds cmd, const char *rawdir, const char *mountpoint, tupledesc_t tuples, unsigned int *r_id) { gpg_error_t err; encfs_parm_t parm; runner_t runner = NULL; - int outbound[2] = { -1, -1 }; - int inbound[2] = { -1, -1 }; const char *pgmname; const char *argv[10]; - pid_t pid = (pid_t)(-1); int idx; + gnupg_process_t proc; + int inbound, outbound; (void)ctrl; parm = xtrycalloc (1, sizeof *parm); if (!parm) { err = gpg_error_from_syserror (); goto leave; } parm->cmd = cmd; parm->tuples = ref_tupledesc (tuples); parm->mountpoint = xtrystrdup (mountpoint); if (!parm->mountpoint) { err = gpg_error_from_syserror (); goto leave; } err = runner_new (&runner, "encfs"); if (err) goto leave; - err = gnupg_create_inbound_pipe (inbound, NULL, 0); - if (!err) - err = gnupg_create_outbound_pipe (outbound, NULL, 0); - if (err) - { - log_error (_("error creating a pipe: %s\n"), gpg_strerror (err)); - goto leave; - } - pgmname = ENCFS; idx = 0; argv[idx++] = "-f"; if (opt.verbose) argv[idx++] = "-v"; argv[idx++] = "--stdinpass"; argv[idx++] = "--annotate"; argv[idx++] = rawdir; argv[idx++] = mountpoint; argv[idx++] = NULL; assert (idx <= DIM (argv)); - err = gnupg_spawn_process_fd (pgmname, argv, - outbound[0], -1, inbound[1], &pid); + err = gnupg_process_spawn (pgmname, argv, + (GNUPG_PROCESS_STDIN_PIPE + | GNUPG_PROCESS_STDERR_PIPE), + NULL, NULL, &proc); if (err) { log_error ("error spawning '%s': %s\n", pgmname, gpg_strerror (err)); goto leave; } - close (outbound[0]); outbound[0] = -1; - close ( inbound[1]); inbound[1] = -1; - runner_set_fds (runner, inbound[0], outbound[1]); - inbound[0] = -1; /* Now owned by RUNNER. */ - outbound[1] = -1; /* Now owned by RUNNER. */ + err = gnupg_process_get_fds (proc, 0, &outbound, NULL, &inbound); + if (err) + { + log_error ("error get fds '%s': %s\n", pgmname, gpg_strerror (err)); + gnupg_process_release (proc); + goto leave; + } + + runner_set_fds (runner, inbound, outbound); runner_set_handler (runner, encfs_handler, encfs_handler_cleanup, parm); parm = NULL; /* Now owned by RUNNER. */ - runner_set_pid (runner, pid); - pid = (pid_t)(-1); /* The process is now owned by RUNNER. */ + runner_set_proc (runner, proc); err = runner_spawn (runner); if (err) - goto leave; + { + gnupg_process_release (proc); + goto leave; + } *r_id = runner_get_rid (runner); log_info ("running '%s' in the background\n", pgmname); leave: - if (inbound[0] != -1) - close (inbound[0]); - if (inbound[1] != -1) - close (inbound[1]); - if (outbound[0] != -1) - close (outbound[0]); - if (outbound[1] != -1) - close (outbound[1]); - if (pid != (pid_t)(-1)) - { - gnupg_wait_process (pgmname, pid, 1, NULL); - gnupg_release_process (pid); - } runner_release (runner); encfs_handler_cleanup (parm); return err; } /* See be_get_detached_name for a description. Note that the dispatcher code makes sure that NULL is stored at R_NAME before calling us. */ gpg_error_t be_encfs_get_detached_name (const char *fname, char **r_name, int *r_isdir) { char *result; if (!fname || !*fname) return gpg_error (GPG_ERR_INV_ARG); result = strconcat (fname, ".d", NULL); if (!result) return gpg_error_from_syserror (); *r_name = result; *r_isdir = 1; return 0; } /* Create a new session key and append it as a tuple to the memory buffer MB. The EncFS daemon takes a passphrase from stdin and internally mangles it by means of some KDF from OpenSSL. We want to store a binary key but we need to make sure that certain characters are not used because the EncFS utility reads it from stdin and obviously acts on some of the characters. This we replace CR (in case of an MSDOS version of EncFS), LF (the delimiter used by EncFS) and Nul (because it is unlikely to work). We use 32 bytes (256 bit) because that is sufficient for the largest cipher (AES-256) and in addition gives enough margin for a possible entropy degradation by the KDF. */ gpg_error_t be_encfs_create_new_keys (membuf_t *mb) { char *buffer; int i, j; /* Allocate a buffer of 32 bytes plus 8 spare bytes we may need to replace the unwanted values. */ buffer = xtrymalloc_secure (32+8); if (!buffer) return gpg_error_from_syserror (); /* Randomize the buffer. STRONG random should be enough as it is a good compromise between security and performance. The anticipated usage of this tool is the quite often creation of new containers and thus this should not deplete the system's entropy tool too much. */ gcry_randomize (buffer, 32+8, GCRY_STRONG_RANDOM); for (i=j=0; i < 32; i++) { if (buffer[i] == '\r' || buffer[i] == '\n' || buffer[i] == 0 ) { /* Replace. */ if (j == 8) { /* Need to get more random. */ gcry_randomize (buffer+32, 8, GCRY_STRONG_RANDOM); j = 0; } buffer[i] = buffer[32+j]; j++; } } /* Store the key. */ append_tuple (mb, KEYBLOB_TAG_ENCKEY, buffer, 32); /* Free the temporary buffer. */ wipememory (buffer, 32+8); /* A failsafe extra wiping. */ xfree (buffer); return 0; } /* Create the container described by the filename FNAME and the keyblob information in TUPLES. */ gpg_error_t be_encfs_create_container (ctrl_t ctrl, const char *fname, tupledesc_t tuples, unsigned int *r_id) { gpg_error_t err; int dummy; char *containername = NULL; char *mountpoint = NULL; err = be_encfs_get_detached_name (fname, &containername, &dummy); if (err) goto leave; mountpoint = xtrystrdup ("/tmp/.#g13_XXXXXX"); if (!mountpoint) { err = gpg_error_from_syserror (); goto leave; } if (!gnupg_mkdtemp (mountpoint)) { err = gpg_error_from_syserror (); log_error (_("can't create directory '%s': %s\n"), "/tmp/.#g13_XXXXXX", gpg_strerror (err)); goto leave; } err = run_encfs_tool (ctrl, ENCFS_CMD_CREATE, containername, mountpoint, tuples, r_id); /* In any case remove the temporary mount point. */ if (rmdir (mountpoint)) log_error ("error removing temporary mount point '%s': %s\n", mountpoint, gpg_strerror (gpg_error_from_syserror ())); leave: xfree (containername); xfree (mountpoint); return err; } /* Mount the container described by the filename FNAME and the keyblob information in TUPLES. On success the runner id is stored at R_ID. */ gpg_error_t be_encfs_mount_container (ctrl_t ctrl, const char *fname, const char *mountpoint, tupledesc_t tuples, unsigned int *r_id) { gpg_error_t err; int dummy; char *containername = NULL; if (!mountpoint) { log_error ("the encfs backend requires an explicit mountpoint\n"); err = gpg_error (GPG_ERR_NOT_SUPPORTED); goto leave; } err = be_encfs_get_detached_name (fname, &containername, &dummy); if (err) goto leave; err = run_encfs_tool (ctrl, ENCFS_CMD_MOUNT, containername, mountpoint, tuples, r_id); leave: xfree (containername); return err; } diff --git a/g13/g13.c b/g13/g13.c index 2bbb453eb..785c91950 100644 --- a/g13/g13.c +++ b/g13/g13.c @@ -1,1056 +1,1057 @@ /* g13.c - Disk Key management with GnuPG * Copyright (C) 2009 Free Software Foundation, Inc. * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include #include #include #include #include #include #include #define INCLUDED_BY_MAIN_MODULE 1 #include "g13.h" #include #include #include "../common/i18n.h" #include "../common/sysutils.h" #include "../common/gc-opt-flags.h" #include "../common/asshelp.h" #include "../common/init.h" +#include "../common/exechelp.h" #include "keyblob.h" #include "server.h" #include "runner.h" #include "create.h" #include "mount.h" #include "suspend.h" #include "mountinfo.h" #include "backend.h" #include "call-syshelp.h" enum cmd_and_opt_values { aNull = 0, oQuiet = 'q', oVerbose = 'v', oRecipient = 'r', aGPGConfList = 500, aGPGConfTest, aCreate, aMount, aUmount, aSuspend, aResume, aServer, aFindDevice, oOptions, oDebug, oDebugLevel, oDebugAll, oDebugNone, oDebugWait, oDebugAllowCoreDump, oLogFile, oNoLogFile, oAuditLog, oOutput, oAgentProgram, oGpgProgram, oType, oDisplay, oTTYname, oTTYtype, oLCctype, oLCmessages, oXauthority, oStatusFD, oLoggerFD, oNoVerbose, oNoSecmemWarn, oNoGreeting, oNoTTY, oNoOptions, oHomedir, oWithColons, oDryRun, oNoDetach, oNoRandomSeedFile, oFakedSystemTime }; static gpgrt_opt_t opts[] = { ARGPARSE_group (300, N_("@Commands:\n ")), ARGPARSE_c (aCreate, "create", N_("Create a new file system container")), ARGPARSE_c (aMount, "mount", N_("Mount a file system container") ), ARGPARSE_c (aUmount, "umount", N_("Unmount a file system container") ), ARGPARSE_c (aSuspend, "suspend", N_("Suspend a file system container") ), ARGPARSE_c (aResume, "resume", N_("Resume a file system container") ), ARGPARSE_c (aServer, "server", N_("Run in server mode")), ARGPARSE_c (aFindDevice, "find-device", "@"), ARGPARSE_c (aGPGConfList, "gpgconf-list", "@"), ARGPARSE_c (aGPGConfTest, "gpgconf-test", "@"), ARGPARSE_group (301, N_("@\nOptions:\n ")), ARGPARSE_s_s (oRecipient, "recipient", N_("|USER-ID|encrypt for USER-ID")), ARGPARSE_s_s (oType, "type", N_("|NAME|use container format NAME")), ARGPARSE_s_s (oOutput, "output", N_("|FILE|write output to FILE")), ARGPARSE_s_n (oVerbose, "verbose", N_("verbose")), ARGPARSE_s_n (oQuiet, "quiet", N_("be somewhat more quiet")), ARGPARSE_s_n (oNoTTY, "no-tty", N_("don't use the terminal at all")), ARGPARSE_s_n (oNoDetach, "no-detach", N_("do not detach from the console")), ARGPARSE_s_s (oLogFile, "log-file", N_("|FILE|write log output to FILE")), ARGPARSE_s_n (oNoLogFile, "no-log-file", "@"), ARGPARSE_s_i (oLoggerFD, "logger-fd", "@"), ARGPARSE_s_n (oDryRun, "dry-run", N_("do not make any changes")), ARGPARSE_conffile (oOptions, "options", N_("|FILE|read options from FILE")), ARGPARSE_s_s (oDebug, "debug", "@"), ARGPARSE_s_s (oDebugLevel, "debug-level", N_("|LEVEL|set the debugging level to LEVEL")), ARGPARSE_s_n (oDebugAll, "debug-all", "@"), ARGPARSE_s_n (oDebugNone, "debug-none", "@"), ARGPARSE_s_i (oDebugWait, "debug-wait", "@"), ARGPARSE_s_n (oDebugAllowCoreDump, "debug-allow-core-dump", "@"), ARGPARSE_s_i (oStatusFD, "status-fd", N_("|FD|write status info to this FD")), ARGPARSE_group (302, N_( "@\n(See the man page for a complete listing of all commands and options)\n" )), ARGPARSE_group (303, N_("@\nExamples:\n\n" " blurb\n" " blurb\n")), /* Hidden options. */ ARGPARSE_s_n (oNoVerbose, "no-verbose", "@"), ARGPARSE_s_n (oNoSecmemWarn, "no-secmem-warning", "@"), ARGPARSE_s_n (oNoGreeting, "no-greeting", "@"), ARGPARSE_noconffile (oNoOptions, "no-options", "@"), ARGPARSE_s_s (oHomedir, "homedir", "@"), ARGPARSE_s_s (oAgentProgram, "agent-program", "@"), ARGPARSE_s_s (oGpgProgram, "gpg-program", "@"), ARGPARSE_s_s (oDisplay, "display", "@"), ARGPARSE_s_s (oTTYname, "ttyname", "@"), ARGPARSE_s_s (oTTYtype, "ttytype", "@"), ARGPARSE_s_s (oLCctype, "lc-ctype", "@"), ARGPARSE_s_s (oLCmessages, "lc-messages", "@"), ARGPARSE_s_s (oXauthority, "xauthority", "@"), ARGPARSE_s_s (oFakedSystemTime, "faked-system-time", "@"), ARGPARSE_s_n (oWithColons, "with-colons", "@"), ARGPARSE_s_n (oNoRandomSeedFile, "no-random-seed-file", "@"), /* Command aliases. */ ARGPARSE_end () }; /* The list of supported debug flags. */ static struct debug_flags_s debug_flags [] = { { DBG_MOUNT_VALUE , "mount" }, { DBG_CRYPTO_VALUE , "crypto" }, { DBG_MEMORY_VALUE , "memory" }, { DBG_MEMSTAT_VALUE, "memstat" }, { DBG_IPC_VALUE , "ipc" }, { 0, NULL } }; /* The timer tick interval used by the idle task. */ #define TIMERTICK_INTERVAL_SEC (1) /* It is possible that we are currently running under setuid permissions. */ static int maybe_setuid = 1; /* Helper to implement --debug-level and --debug. */ static const char *debug_level; static unsigned int debug_value; /* Flag to indicate that a shutdown was requested. */ static int shutdown_pending; /* The thread id of the idle task. */ static npth_t idle_task_thread; /* The container type as specified on the command line. */ static int cmdline_conttype; static void set_cmd (enum cmd_and_opt_values *ret_cmd, enum cmd_and_opt_values new_cmd ); static void start_idle_task (void); static void join_idle_task (void); /* Begin NPth wrapper functions. */ ASSUAN_SYSTEM_NPTH_IMPL; static const char * my_strusage( int level ) { const char *p; switch (level) { case 9: p = "GPL-3.0-or-later"; break; case 11: p = "@G13@ (@GNUPG@)"; break; case 13: p = VERSION; break; case 14: p = GNUPG_DEF_COPYRIGHT_LINE; break; case 17: p = PRINTABLE_OS_NAME; break; case 19: p = _("Please report bugs to <" PACKAGE_BUGREPORT ">.\n"); break; case 1: case 40: p = _("Usage: @G13@ [options] [files] (-h for help)"); break; case 41: p = _("Syntax: @G13@ [options] [files]\n" "Create, mount or unmount an encrypted file system container\n"); break; case 31: p = "\nHome: "; break; case 32: p = gnupg_homedir (); break; default: p = NULL; break; } return p; } static void wrong_args (const char *text) { fprintf (stderr, _("usage: %s [options] "), G13_NAME); fputs (text, stderr); putc ('\n', stderr); g13_exit (2); } /* Setup the debugging. With a DEBUG_LEVEL of NULL only the active debug flags are propagated to the subsystems. With DEBUG_LEVEL set, a specific set of debug flags is set; and individual debugging flags will be added on top. */ static void set_debug (void) { int numok = (debug_level && digitp (debug_level)); int numlvl = numok? atoi (debug_level) : 0; if (!debug_level) ; else if (!strcmp (debug_level, "none") || (numok && numlvl < 1)) opt.debug = 0; else if (!strcmp (debug_level, "basic") || (numok && numlvl <= 2)) opt.debug = DBG_IPC_VALUE|DBG_MOUNT_VALUE; else if (!strcmp (debug_level, "advanced") || (numok && numlvl <= 5)) opt.debug = DBG_IPC_VALUE|DBG_MOUNT_VALUE; else if (!strcmp (debug_level, "expert") || (numok && numlvl <= 8)) opt.debug = (DBG_IPC_VALUE|DBG_MOUNT_VALUE|DBG_CRYPTO_VALUE); else if (!strcmp (debug_level, "guru") || numok) { opt.debug = ~0; /* if (numok) */ /* opt.debug &= ~(DBG_HASHING_VALUE); */ } else { log_error (_("invalid debug-level '%s' given\n"), debug_level); g13_exit(2); } opt.debug |= debug_value; if (opt.debug && !opt.verbose) opt.verbose = 1; if (opt.debug) opt.quiet = 0; if (opt.debug & DBG_CRYPTO_VALUE ) gcry_control (GCRYCTL_SET_DEBUG_FLAGS, 1); gcry_control (GCRYCTL_SET_VERBOSITY, (int)opt.verbose); if (opt.debug) parse_debug_flag (NULL, &opt.debug, debug_flags); } static void set_cmd (enum cmd_and_opt_values *ret_cmd, enum cmd_and_opt_values new_cmd) { enum cmd_and_opt_values cmd = *ret_cmd; if (!cmd || cmd == new_cmd) cmd = new_cmd; else { log_error (_("conflicting commands\n")); g13_exit (2); } *ret_cmd = cmd; } int main (int argc, char **argv) { gpgrt_argparse_t pargs; int orig_argc; char **orig_argv; gpg_error_t err = 0; /* const char *fname; */ int may_coredump; char *last_configname = NULL; const char *configname = NULL; int debug_argparser = 0; int no_more_options = 0; char *logfile = NULL; int greeting = 0; int nogreeting = 0; /* int debug_wait = 0; */ int use_random_seed = 1; /* int nodetach = 0; */ /* int nokeysetup = 0; */ enum cmd_and_opt_values cmd = 0; struct server_control_s ctrl; strlist_t recipients = NULL; /*mtrace();*/ early_system_init (); gnupg_reopen_std (G13_NAME); gpgrt_set_strusage (my_strusage); gcry_control (GCRYCTL_SUSPEND_SECMEM_WARN); log_set_prefix (G13_NAME, GPGRT_LOG_WITH_PREFIX); /* Make sure that our subsystems are ready. */ i18n_init (); init_common_subsystems (&argc, &argv); npth_init (); /* Take extra care of the random pool. */ gcry_control (GCRYCTL_USE_SECURE_RNDPOOL); may_coredump = disable_core_dumps (); g13_init_signals (); dotlock_create (NULL, 0); /* Register locking cleanup. */ opt.session_env = session_env_new (); if (!opt.session_env) log_fatal ("error allocating session environment block: %s\n", strerror (errno)); /* First check whether we have a config file on the commandline. */ orig_argc = argc; orig_argv = argv; pargs.argc = &argc; pargs.argv = &argv; pargs.flags= (ARGPARSE_FLAG_KEEP | ARGPARSE_FLAG_NOVERSION); while (gpgrt_argparse (NULL, &pargs, opts)) { switch (pargs.r_opt) { case oDebug: case oDebugAll: debug_argparser++; break; case oHomedir: gnupg_set_homedir (pargs.r.ret_str); break; } } /* Reset the flags. */ pargs.flags &= ~(ARGPARSE_FLAG_KEEP | ARGPARSE_FLAG_NOVERSION); /* Initialize the secure memory. */ gcry_control (GCRYCTL_INIT_SECMEM, 16384, 0); maybe_setuid = 0; /* * Now we are now working under our real uid */ /* Setup malloc hooks. */ { struct assuan_malloc_hooks malloc_hooks; malloc_hooks.malloc = gcry_malloc; malloc_hooks.realloc = gcry_realloc; malloc_hooks.free = gcry_free; assuan_set_malloc_hooks (&malloc_hooks); } /* Prepare libassuan. */ assuan_set_gpg_err_source (GPG_ERR_SOURCE_DEFAULT); assuan_set_system_hooks (ASSUAN_SYSTEM_NPTH); setup_libassuan_logging (&opt.debug, NULL); /* Setup a default control structure for command line mode. */ memset (&ctrl, 0, sizeof ctrl); g13_init_default_ctrl (&ctrl); ctrl.no_server = 1; ctrl.status_fd = -1; /* No status output. */ /* The configuraton directories for use by gpgrt_argparser. */ gpgrt_set_confdir (GPGRT_CONFDIR_SYS, gnupg_sysconfdir ()); gpgrt_set_confdir (GPGRT_CONFDIR_USER, gnupg_homedir ()); /* We are re-using the struct, thus the reset flag. We OR the * flags so that the internal intialized flag won't be cleared. */ argc = orig_argc; argv = orig_argv; pargs.argc = &argc; pargs.argv = &argv; pargs.flags |= (ARGPARSE_FLAG_RESET | ARGPARSE_FLAG_KEEP | ARGPARSE_FLAG_SYS | ARGPARSE_FLAG_USER); while (!no_more_options && gpgrt_argparser (&pargs, opts, G13_NAME EXTSEP_S "conf")) { switch (pargs.r_opt) { case ARGPARSE_CONFFILE: { if (debug_argparser) log_info (_("reading options from '%s'\n"), pargs.r_type? pargs.r.ret_str: "[cmdline]"); if (pargs.r_type) { xfree (last_configname); last_configname = xstrdup (pargs.r.ret_str); configname = last_configname; } else configname = NULL; } break; case aGPGConfList: case aGPGConfTest: set_cmd (&cmd, pargs.r_opt); nogreeting = 1; /* nokeysetup = 1; */ break; case aServer: case aMount: case aUmount: case aSuspend: case aResume: case aCreate: case aFindDevice: set_cmd (&cmd, pargs.r_opt); break; case oOutput: opt.outfile = pargs.r.ret_str; break; case oQuiet: opt.quiet = 1; break; case oNoGreeting: nogreeting = 1; break; case oNoTTY: break; case oDryRun: opt.dry_run = 1; break; case oVerbose: opt.verbose++; gcry_control (GCRYCTL_SET_VERBOSITY, (int)opt.verbose); break; case oNoVerbose: opt.verbose = 0; gcry_control (GCRYCTL_SET_VERBOSITY, (int)opt.verbose); break; case oLogFile: logfile = pargs.r.ret_str; break; case oNoLogFile: logfile = NULL; break; case oNoDetach: /*nodetach = 1; */break; case oDebug: if (parse_debug_flag (pargs.r.ret_str, &opt.debug, debug_flags)) { pargs.r_opt = ARGPARSE_INVALID_ARG; pargs.err = ARGPARSE_PRINT_ERROR; } break; case oDebugAll: debug_value = ~0; break; case oDebugNone: debug_value = 0; break; case oDebugLevel: debug_level = pargs.r.ret_str; break; case oDebugWait: /*debug_wait = pargs.r.ret_int; */break; case oDebugAllowCoreDump: may_coredump = enable_core_dumps (); break; case oStatusFD: ctrl.status_fd = pargs.r.ret_int; break; case oLoggerFD: log_set_fd (pargs.r.ret_int ); break; case oHomedir: gnupg_set_homedir (pargs.r.ret_str); break; case oAgentProgram: opt.agent_program = pargs.r.ret_str; break; case oGpgProgram: opt.gpg_program = pargs.r.ret_str; break; case oDisplay: opt.display = xstrdup (pargs.r.ret_str); break; case oTTYname: opt.ttyname = xstrdup (pargs.r.ret_str); break; case oTTYtype: opt.ttytype = xstrdup (pargs.r.ret_str); break; case oLCctype: opt.lc_ctype = xstrdup (pargs.r.ret_str); break; case oLCmessages: opt.lc_messages = xstrdup (pargs.r.ret_str); break; case oXauthority: opt.xauthority = xstrdup (pargs.r.ret_str); break; case oFakedSystemTime: { time_t faked_time = isotime2epoch (pargs.r.ret_str); if (faked_time == (time_t)(-1)) faked_time = (time_t)strtoul (pargs.r.ret_str, NULL, 10); gnupg_set_time (faked_time, 0); } break; case oNoSecmemWarn: gcry_control (GCRYCTL_DISABLE_SECMEM_WARN); break; case oNoRandomSeedFile: use_random_seed = 0; break; case oRecipient: /* Store the encryption key. */ add_to_strlist (&recipients, pargs.r.ret_str); break; case oType: if (!strcmp (pargs.r.ret_str, "help")) { be_parse_conttype_name (NULL); g13_exit (0); } cmdline_conttype = be_parse_conttype_name (pargs.r.ret_str); if (!cmdline_conttype) { pargs.r_opt = ARGPARSE_INVALID_ARG; pargs.err = ARGPARSE_PRINT_ERROR; } break; default: if (configname) pargs.err = ARGPARSE_PRINT_WARNING; else pargs.err = ARGPARSE_PRINT_ERROR; break; } } gpgrt_argparse (NULL, &pargs, NULL); /* Construct GPG arguments. */ { strlist_t last; last = append_to_strlist (&opt.gpg_arguments, "-z"); last = append_to_strlist (&last, "0"); last = append_to_strlist (&last, "--trust-model"); last = append_to_strlist (&last, "always"); (void) last; } if (!last_configname) opt.config_filename = gpgrt_fnameconcat (gnupg_homedir (), G13_NAME EXTSEP_S "conf", NULL); else { opt.config_filename = last_configname; last_configname = NULL; } if (log_get_errorcount(0)) g13_exit(2); /* Now that we have the options parsed we need to update the default control structure. */ g13_init_default_ctrl (&ctrl); ctrl.recipients = recipients; recipients = NULL; if (nogreeting) greeting = 0; if (greeting) { fprintf (stderr, "%s %s; %s\n", gpgrt_strusage(11), gpgrt_strusage(13), gpgrt_strusage(14) ); fprintf (stderr, "%s\n", gpgrt_strusage(15)); } if (may_coredump && !opt.quiet) log_info (_("WARNING: program may create a core file!\n")); /* Print a warning if an argument looks like an option. */ if (!opt.quiet && !(pargs.flags & ARGPARSE_FLAG_STOP_SEEN)) { int i; for (i=0; i < argc; i++) if (argv[i][0] == '-' && argv[i][1] == '-') log_info (_("Note: '%s' is not considered an option\n"), argv[i]); } if (logfile) { log_set_file (logfile); log_set_prefix (NULL, (GPGRT_LOG_WITH_PREFIX | GPGRT_LOG_WITH_TIME | GPGRT_LOG_WITH_PID)); } if (gnupg_faked_time_p ()) { gnupg_isotime_t tbuf; log_info (_("WARNING: running with faked system time: ")); gnupg_get_isotime (tbuf); dump_isotime (tbuf); log_printf ("\n"); } /* Print any pending secure memory warnings. */ gcry_control (GCRYCTL_RESUME_SECMEM_WARN); /* Setup the debug flags for all subsystems. */ set_debug (); /* Install emergency cleanup handler. */ g13_install_emergency_cleanup (); /* Terminate if we found any error until now. */ if (log_get_errorcount(0)) g13_exit (2); /* Set the standard GnuPG random seed file. */ if (use_random_seed) { char *p = make_filename (gnupg_homedir (), "random_seed", NULL); gcry_control (GCRYCTL_SET_RANDOM_SEED_FILE, p); xfree(p); } /* Store given filename into FNAME. */ /* fname = argc? *argv : NULL; */ /* Parse all given encryption keys. This does a lookup of the keys and stops if any of the given keys was not found. */ #if 0 /* Currently not implemented. */ if (!nokeysetup) { strlist_t sl; int failed = 0; for (sl = ctrl->recipients; sl; sl = sl->next) if (check_encryption_key ()) failed = 1; if (failed) g13_exit (1); } #endif /*0*/ /* Dispatch command. */ err = 0; switch (cmd) { case aGPGConfList: { /* List options and default values in the GPG Conf format. */ char *config_filename_esc = percent_escape (opt.config_filename, NULL); printf ("gpgconf-g13.conf:%lu:\"%s\n", GC_OPT_FLAG_DEFAULT, config_filename_esc); xfree (config_filename_esc); printf ("verbose:%lu:\n", GC_OPT_FLAG_NONE); printf ("quiet:%lu:\n", GC_OPT_FLAG_NONE); printf ("debug-level:%lu:\"none:\n", GC_OPT_FLAG_DEFAULT); printf ("log-file:%lu:\n", GC_OPT_FLAG_NONE); } break; case aGPGConfTest: /* This is merely a dummy command to test whether the configuration file is valid. */ break; case aServer: { start_idle_task (); ctrl.no_server = 0; err = g13_server (&ctrl); if (err) log_error ("server exited with error: %s <%s>\n", gpg_strerror (err), gpg_strsource (err)); else g13_request_shutdown (); } break; case aFindDevice: { char *blockdev; if (argc != 1) wrong_args ("--find-device name"); err = call_syshelp_find_device (&ctrl, argv[0], &blockdev); if (err) log_error ("error finding device '%s': %s <%s>\n", argv[0], gpg_strerror (err), gpg_strsource (err)); else puts (blockdev); } break; case aCreate: /* Create a new container. */ { if (argc != 1) wrong_args ("--create filename"); start_idle_task (); err = g13_create_container (&ctrl, argv[0]); if (err) log_error ("error creating a new container: %s <%s>\n", gpg_strerror (err), gpg_strsource (err)); else g13_request_shutdown (); } break; case aMount: /* Mount a container. */ { if (argc != 1 && argc != 2 ) wrong_args ("--mount filename [mountpoint]"); start_idle_task (); err = g13_mount_container (&ctrl, argv[0], argc == 2?argv[1]:NULL); if (err) log_error ("error mounting container '%s': %s <%s>\n", *argv, gpg_strerror (err), gpg_strsource (err)); } break; case aUmount: /* Unmount a mounted container. */ { if (argc != 1) wrong_args ("--umount filename"); err = g13_umount_container (&ctrl, argv[0], NULL); if (err) log_error ("error unmounting container '%s': %s <%s>\n", *argv, gpg_strerror (err), gpg_strsource (err)); } break; case aSuspend: /* Suspend a container. */ { /* Fixme: Should we add a suspend all container option? */ if (argc != 1) wrong_args ("--suspend filename"); err = g13_suspend_container (&ctrl, argv[0]); if (err) log_error ("error suspending container '%s': %s <%s>\n", *argv, gpg_strerror (err), gpg_strsource (err)); } break; case aResume: /* Resume a suspended container. */ { /* Fixme: Should we add a resume all container option? */ if (argc != 1) wrong_args ("--resume filename"); err = g13_resume_container (&ctrl, argv[0]); if (err) log_error ("error resuming container '%s': %s <%s>\n", *argv, gpg_strerror (err), gpg_strsource (err)); } break; default: log_error (_("invalid command (there is no implicit command)\n")); break; } g13_deinit_default_ctrl (&ctrl); if (!err) join_idle_task (); /* Cleanup. */ g13_exit (0); return 8; /*NOTREACHED*/ } /* Store defaults into the per-connection CTRL object. */ void g13_init_default_ctrl (ctrl_t ctrl) { ctrl->conttype = cmdline_conttype? cmdline_conttype : CONTTYPE_ENCFS; } /* Release remaining resources allocated in the CTRL object. */ void g13_deinit_default_ctrl (ctrl_t ctrl) { call_syshelp_release (ctrl); FREE_STRLIST (ctrl->recipients); } /* Request a shutdown. This can be used when the process should * finish instead of running the idle task. */ void g13_request_shutdown (void) { shutdown_pending++; } /* This function is called for each signal we catch. It is run in the main context or the one of a NPth thread and thus it is not restricted in what it may do. */ static void handle_signal (int signo) { switch (signo) { #ifndef HAVE_W32_SYSTEM case SIGHUP: log_info ("SIGHUP received - re-reading configuration\n"); /* Fixme: Not yet implemented. */ break; case SIGUSR1: log_info ("SIGUSR1 received - printing internal information:\n"); /* Fixme: We need to see how to integrate pth dumping into our logging system. */ /* pth_ctrl (PTH_CTRL_DUMPSTATE, log_get_stream ()); */ mountinfo_dump_all (); break; case SIGUSR2: log_info ("SIGUSR2 received - no action defined\n"); break; case SIGTERM: if (!shutdown_pending) log_info ("SIGTERM received - shutting down ...\n"); else log_info ("SIGTERM received - still %u runners active\n", runner_get_threads ()); shutdown_pending++; if (shutdown_pending > 2) { log_info ("shutdown forced\n"); log_info ("%s %s stopped\n", gpgrt_strusage(11), gpgrt_strusage(13) ); g13_exit (0); } break; case SIGINT: log_info ("SIGINT received - immediate shutdown\n"); log_info( "%s %s stopped\n", gpgrt_strusage(11), gpgrt_strusage(13)); g13_exit (0); break; #endif /*!HAVE_W32_SYSTEM*/ default: log_info ("signal %d received - no action defined\n", signo); } } /* This ticker function is called about every TIMERTICK_INTERVAL_SEC seconds. */ static void handle_tick (void) { /* log_debug ("TICK\n"); */ } /* The idle task. We use a separate thread to do idle stuff and to catch signals. */ static void * idle_task (void *dummy_arg) { int signo; /* The number of a raised signal is stored here. */ int saved_errno; struct timespec abstime; struct timespec curtime; struct timespec timeout; int ret; (void)dummy_arg; /* Create the event to catch the signals. */ #ifndef HAVE_W32_SYSTEM npth_sigev_init (); npth_sigev_add (SIGHUP); npth_sigev_add (SIGUSR1); npth_sigev_add (SIGUSR2); npth_sigev_add (SIGINT); npth_sigev_add (SIGTERM); npth_sigev_fini (); #endif npth_clock_gettime (&abstime); abstime.tv_sec += TIMERTICK_INTERVAL_SEC; for (;;) { /* The shutdown flag allows us to terminate the idle task. */ if (shutdown_pending) { runner_cancel_all (); if (!runner_get_threads ()) break; /* ready */ } npth_clock_gettime (&curtime); if (!(npth_timercmp (&curtime, &abstime, <))) { /* Timeout. */ handle_tick (); npth_clock_gettime (&abstime); abstime.tv_sec += TIMERTICK_INTERVAL_SEC; } npth_timersub (&abstime, &curtime, &timeout); #ifndef HAVE_W32_SYSTEM ret = npth_pselect (0, NULL, NULL, NULL, &timeout, npth_sigev_sigmask()); saved_errno = errno; while (npth_sigev_get_pending(&signo)) handle_signal (signo); #else ret = npth_eselect (0, NULL, NULL, NULL, &timeout, NULL, NULL); saved_errno = errno; #endif if (ret == -1 && saved_errno != EINTR) { log_error (_("npth_pselect failed: %s - waiting 1s\n"), strerror (saved_errno)); npth_sleep (1); continue; } if (ret <= 0) { /* Interrupt or timeout. Will be handled when calculating the next timeout. */ continue; } /* Here one would add processing of file descriptors. */ } log_info (_("%s %s stopped\n"), gpgrt_strusage(11), gpgrt_strusage(13)); return NULL; } /* Start the idle task. */ static void start_idle_task (void) { npth_attr_t tattr; npth_t thread; sigset_t sigs; /* The set of signals we want to catch. */ int err; #ifndef HAVE_W32_SYSTEM /* These signals should always go to the idle task, so they need to be blocked everywhere else. We assume start_idle_task is called from the main thread before any other threads are created. */ sigemptyset (&sigs); sigaddset (&sigs, SIGHUP); sigaddset (&sigs, SIGUSR1); sigaddset (&sigs, SIGUSR2); sigaddset (&sigs, SIGINT); sigaddset (&sigs, SIGTERM); npth_sigmask (SIG_BLOCK, &sigs, NULL); #endif npth_attr_init (&tattr); npth_attr_setdetachstate (&tattr, NPTH_CREATE_JOINABLE); err = npth_create (&thread, &tattr, idle_task, NULL); if (err) { log_fatal ("error starting idle task: %s\n", strerror (err)); return; /*NOTREACHED*/ } npth_setname_np (thread, "idle-task"); idle_task_thread = thread; npth_attr_destroy (&tattr); } /* Wait for the idle task to finish. */ static void join_idle_task (void) { int err; /* FIXME: This assumes that a valid pthread_t is non-null. That is not guaranteed. */ if (idle_task_thread) { err = npth_join (idle_task_thread, NULL); if (err) log_error ("waiting for idle task thread failed: %s\n", strerror (err)); } } diff --git a/g13/mount.c b/g13/mount.c index 45b60806c..071b76b67 100644 --- a/g13/mount.c +++ b/g13/mount.c @@ -1,265 +1,266 @@ /* mount.c - Mount a crypto container * Copyright (C) 2009 Free Software Foundation, Inc. * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include "g13.h" #include "../common/i18n.h" #include "mount.h" #include "keyblob.h" #include "backend.h" #include "g13tuple.h" #include "mountinfo.h" -#include "runner.h" #include "../common/host2net.h" #include "server.h" /*(g13_keyblob_decrypt)*/ #include "../common/sysutils.h" +#include "../common/exechelp.h" +#include "runner.h" #include "call-syshelp.h" /* Mount the container with name FILENAME at MOUNTPOINT. */ gpg_error_t g13_mount_container (ctrl_t ctrl, const char *filename, const char *mountpoint) { gpg_error_t err; dotlock_t lock; int needs_syshelp = 0; void *enckeyblob = NULL; size_t enckeybloblen; void *keyblob = NULL; size_t keybloblen; tupledesc_t tuples = NULL; size_t n; const unsigned char *value; int conttype; unsigned int rid; char *mountpoint_buffer = NULL; char *blockdev_buffer = NULL; /* Decide whether we need to use the g13-syshelp. */ err = call_syshelp_find_device (ctrl, filename, &blockdev_buffer); if (!err) { needs_syshelp = 1; filename = blockdev_buffer; } else if (gpg_err_code (err) != GPG_ERR_NOT_FOUND) { log_error ("error finding device '%s': %s <%s>\n", filename, gpg_strerror (err), gpg_strsource (err)); return err; } else { /* A quick check to see whether we can the container exists. */ if (gnupg_access (filename, R_OK)) return gpg_error_from_syserror (); } if (!mountpoint) { mountpoint_buffer = xtrystrdup ("/tmp/g13-XXXXXX"); if (!mountpoint_buffer) return gpg_error_from_syserror (); if (!gnupg_mkdtemp (mountpoint_buffer)) { err = gpg_error_from_syserror (); log_error (_("can't create directory '%s': %s\n"), "/tmp/g13-XXXXXX", gpg_strerror (err)); xfree (mountpoint_buffer); return err; } mountpoint = mountpoint_buffer; } err = 0; if (needs_syshelp) lock = NULL; else { /* Try to take a lock. */ lock = dotlock_create (filename, 0); if (!lock) { xfree (mountpoint_buffer); return gpg_error_from_syserror (); } if (dotlock_take (lock, 0)) { err = gpg_error_from_syserror (); goto leave; } } /* Check again that the file exists. */ if (!needs_syshelp) { struct stat sb; if (gnupg_stat (filename, &sb)) { err = gpg_error_from_syserror (); goto leave; } } /* Read the encrypted keyblob. */ if (needs_syshelp) { err = call_syshelp_set_device (ctrl, filename); if (err) goto leave; err = call_syshelp_get_keyblob (ctrl, &enckeyblob, &enckeybloblen); } else err = g13_keyblob_read (filename, &enckeyblob, &enckeybloblen); if (err) goto leave; /* Decrypt that keyblob and store it in a tuple descriptor. */ err = g13_keyblob_decrypt (ctrl, enckeyblob, enckeybloblen, &keyblob, &keybloblen); if (err) goto leave; xfree (enckeyblob); enckeyblob = NULL; err = create_tupledesc (&tuples, keyblob, keybloblen); if (!err) keyblob = NULL; else { if (gpg_err_code (err) == GPG_ERR_NOT_SUPPORTED) log_error ("unknown keyblob version\n"); goto leave; } if (opt.verbose) dump_tupledesc (tuples); value = find_tuple (tuples, KEYBLOB_TAG_CONTTYPE, &n); if (!value || n != 2) conttype = 0; else conttype = (value[0] << 8 | value[1]); if (!be_is_supported_conttype (conttype)) { log_error ("content type %d is not supported\n", conttype); err = gpg_error (GPG_ERR_NOT_SUPPORTED); goto leave; } err = be_mount_container (ctrl, conttype, filename, mountpoint, tuples, &rid); if (err) ; else if (conttype == CONTTYPE_DM_CRYPT) g13_request_shutdown (); else { /* Unless this is a DM-CRYPT mount we put it into our mounttable so that we can manage the mounts ourselves. For dm-crypt we do not keep a process to monitor he mounts (for now). */ err = mountinfo_add_mount (filename, mountpoint, conttype, rid, !!mountpoint_buffer); /* Fixme: What shall we do if this fails? Add a provisional mountinfo entry first and remove it on error? */ if (!err) { char *tmp = percent_plus_escape (mountpoint); if (!tmp) err = gpg_error_from_syserror (); else { g13_status (ctrl, STATUS_MOUNTPOINT, tmp, NULL); xfree (tmp); } } } leave: destroy_tupledesc (tuples); xfree (keyblob); xfree (enckeyblob); dotlock_destroy (lock); xfree (mountpoint_buffer); xfree (blockdev_buffer); return err; } /* Unmount the container with name FILENAME or the one mounted at MOUNTPOINT. If both are given the FILENAME takes precedence. */ gpg_error_t g13_umount_container (ctrl_t ctrl, const char *filename, const char *mountpoint) { gpg_error_t err; char *blockdev; if (!filename && !mountpoint) return gpg_error (GPG_ERR_ENOENT); /* Decide whether we need to use the g13-syshelp. */ err = call_syshelp_find_device (ctrl, filename, &blockdev); if (!err) { /* Need to employ the syshelper to umount the file system. */ /* FIXME: We should get the CONTTYPE from the blockdev. */ err = be_umount_container (ctrl, CONTTYPE_DM_CRYPT, blockdev); if (!err) { /* if (conttype == CONTTYPE_DM_CRYPT) */ g13_request_shutdown (); } } else if (gpg_err_code (err) != GPG_ERR_NOT_FOUND) { log_error ("error finding device '%s': %s <%s>\n", filename, gpg_strerror (err), gpg_strsource (err)); } else { /* Not in g13tab - kill the runner process for this mount. */ unsigned int rid; runner_t runner; err = mountinfo_find_mount (filename, mountpoint, &rid); if (err) return err; runner = runner_find_by_rid (rid); if (!runner) { log_error ("runner %u not found\n", rid); return gpg_error (GPG_ERR_NOT_FOUND); } runner_cancel (runner); runner_release (runner); } xfree (blockdev); return err; } diff --git a/g13/runner.c b/g13/runner.c index b08d99030..c0534fe5d 100644 --- a/g13/runner.c +++ b/g13/runner.c @@ -1,539 +1,541 @@ /* runner.c - Run and watch the backend engines * Copyright (C) 2009 Free Software Foundation, Inc. * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include "g13.h" #include "../common/i18n.h" #include "keyblob.h" -#include "runner.h" #include "../common/exechelp.h" +#include "runner.h" #include "mountinfo.h" /* The runner object. */ struct runner_s { char *name; /* The name of this runner. */ unsigned int identifier; /* The runner identifier. */ int spawned; /* True if runner_spawn has been called. */ npth_t thread; /* The TID of the runner thread. */ runner_t next_running; /* Builds a list of all running threads. */ int canceled; /* Set if a cancel has already been send once. */ int cancel_flag; /* If set the thread should terminate itself. */ /* We use a reference counter to know when it is safe to remove the object. Lacking an explicit ref function this counter will take only these two values: 1 = Thread not running or only the thread is still running. 2 = Thread is running and someone is holding a reference. */ int refcount; - pid_t pid; /* PID of the backend's process (the engine). */ + gnupg_process_t proc; /* Process of the backend's process (the engine). */ int in_fd; /* File descriptors to read from the engine. */ int out_fd; /* File descriptors to write to the engine. */ engine_handler_fnc_t handler; /* The handler functions. */ engine_handler_cleanup_fnc_t handler_cleanup; void *handler_data; /* Private data of HANDLER and HANDLER_CLEANUP. */ /* Instead of IN_FD we use an estream. Note that the runner thread may close the stream and set status_fp to NULL at any time. Thus it won't be a good idea to use it while the runner thread is running. */ estream_t status_fp; }; /* The head of the list of all running threads. */ static runner_t running_threads; /* Write NBYTES of BUF to file descriptor FD. */ static int writen (int fd, const void *buf, size_t nbytes) { size_t nleft = nbytes; int nwritten; while (nleft > 0) { nwritten = npth_write (fd, buf, nleft); if (nwritten < 0) { if (errno == EINTR) nwritten = 0; else return -1; } nleft -= nwritten; buf = (const char*)buf + nwritten; } return 0; } static int check_already_spawned (runner_t runner, const char *funcname) { if (runner->spawned) { log_error ("BUG: runner already spawned - ignoring call to %s\n", funcname); return 1; } else return 0; } /* Return the number of active threads. */ unsigned int runner_get_threads (void) { unsigned int n = 0; runner_t r; for (r = running_threads; r; r = r->next_running) n++; return n; } /* The public release function. */ void runner_release (runner_t runner) { gpg_error_t err; if (!runner) return; if (!--runner->refcount) return; err = mountinfo_del_mount (NULL, NULL, runner->identifier); if (err) log_error ("failed to remove mount with rid %u from mtab: %s\n", runner->identifier, gpg_strerror (err)); es_fclose (runner->status_fp); if (runner->in_fd != -1) close (runner->in_fd); if (runner->out_fd != -1) close (runner->out_fd); /* Fixme: close the process. */ /* Tell the engine to release its data. */ if (runner->handler_cleanup) runner->handler_cleanup (runner->handler_data); - if (runner->pid != (pid_t)(-1)) + if (runner->proc) { /* The process has not been cleaned up - do it now. */ - gnupg_kill_process (runner->pid); + gnupg_process_terminate (runner->proc); /* (Actually we should use the program name and not the arbitrary NAME of the runner object. However it does not matter because that information is only used for diagnostics.) */ - gnupg_wait_process (runner->name, runner->pid, 1, NULL); - gnupg_release_process (runner->pid); + gnupg_process_wait (runner->proc, 1); + gnupg_process_release (runner->proc); } xfree (runner->name); xfree (runner); } /* Create a new runner context. On success a new runner object is stored at R_RUNNER. On failure NULL is stored at this address and an error code returned. */ gpg_error_t runner_new (runner_t *r_runner, const char *name) { static unsigned int namecounter; /* Global name counter. */ char *namebuffer; runner_t runner, r; *r_runner = NULL; runner = xtrycalloc (1, sizeof *runner); if (!runner) return gpg_error_from_syserror (); /* Bump up the namecounter. In case we ever had an overflow we check that this number is currently not in use. The algorithm is a bit lame but should be sufficient because such an wrap is not very likely: Assuming that we do a mount 10 times a second, then we would overwrap on a 32 bit system after 13 years. */ do { namecounter++; for (r = running_threads; r; r = r->next_running) if (r->identifier == namecounter) break; } while (r); runner->identifier = namecounter; runner->name = namebuffer = xtryasprintf ("%s-%d", name, namecounter); if (!runner->name) { xfree (runner); return gpg_error_from_syserror (); } runner->refcount = 1; - runner->pid = (pid_t)(-1); + runner->proc = NULL; runner->in_fd = -1; runner->out_fd = -1; *r_runner = runner; return 0; } /* Return the identifier of RUNNER. */ unsigned int runner_get_rid (runner_t runner) { return runner->identifier; } /* Find a runner by its rid. Returns the runner object. The caller must release the runner object. */ runner_t runner_find_by_rid (unsigned int rid) { runner_t r; for (r = running_threads; r; r = r->next_running) if (r->identifier == rid) { r->refcount++; return r; } return NULL; } /* A runner usually maintains two file descriptors to control the backend engine. This function is used to set these file descriptors. The function takes ownership of these file descriptors. IN_FD will be used to read from engine and OUT_FD to send data to the engine. */ void runner_set_fds (runner_t runner, int in_fd, int out_fd) { if (check_already_spawned (runner, "runner_set_fds")) return; if (runner->in_fd != -1) close (runner->in_fd); if (runner->out_fd != -1) close (runner->out_fd); runner->in_fd = in_fd; runner->out_fd = out_fd; } -/* Set the PID of the backend engine. After this call the engine is +/* Set the PROC of the backend engine. After this call the engine is owned by the runner object. */ void -runner_set_pid (runner_t runner, pid_t pid) +runner_set_proc (runner_t runner, gnupg_process_t proc) { - if (check_already_spawned (runner, "runner_set_fds")) + if (check_already_spawned (runner, "runner_set_proc")) return; - runner->pid = pid; + runner->proc = proc; } /* Register the engine handler functions HANDLER and HANDLER_CLEANUP and its private HANDLER_DATA with RUNNER. */ void runner_set_handler (runner_t runner, engine_handler_fnc_t handler, engine_handler_cleanup_fnc_t handler_cleanup, void *handler_data) { if (check_already_spawned (runner, "runner_set_handler")) return; runner->handler = handler; runner->handler_cleanup = handler_cleanup; runner->handler_data = handler_data; } /* The thread spawned by runner_spawn. */ static void * runner_thread (void *arg) { runner_t runner = arg; gpg_error_t err = 0; log_debug ("starting runner thread\n"); /* If a status_fp is available, the thread's main task is to read from that stream and invoke the backend's handler function. This is done on a line by line base and the line length is limited to a reasonable value (about 1000 characters). Other work will continue either due to an EOF of the stream or by demand of the engine. */ if (runner->status_fp) { int c, cont_line; unsigned int pos; char buffer[1024]; estream_t fp = runner->status_fp; pos = 0; cont_line = 0; while (!err && !runner->cancel_flag && (c=es_getc (fp)) != EOF) { buffer[pos++] = c; if (pos >= sizeof buffer - 5 || c == '\n') { buffer[pos - (c == '\n')] = 0; if (opt.verbose) log_info ("%s%s: %s\n", runner->name, cont_line? "(cont)":"", buffer); /* We handle only complete lines and ignore any stuff we possibly had to truncate. That is - at least for the encfs engine - not an issue because our changes to the tool make sure that only relatively short prompt lines are of interest. */ if (!cont_line && runner->handler) err = runner->handler (runner->handler_data, runner, buffer); pos = 0; cont_line = (c != '\n'); } } if (!err && runner->cancel_flag) log_debug ("runner thread noticed cancel flag\n"); else log_debug ("runner thread saw EOF\n"); if (pos) { buffer[pos] = 0; if (opt.verbose) log_info ("%s%s: %s\n", runner->name, cont_line? "(cont)":"", buffer); if (!cont_line && !err && runner->handler) err = runner->handler (runner->handler_data, runner, buffer); } if (!err && es_ferror (fp)) { err = gpg_error_from_syserror (); log_error ("error reading from %s: %s\n", runner->name, gpg_strerror (err)); } runner->status_fp = NULL; es_fclose (fp); log_debug ("runner thread closed status fp\n"); } /* Now wait for the process to finish. */ - if (!err && runner->pid != (pid_t)(-1)) + if (!err && runner->proc) { int exitcode; log_debug ("runner thread waiting ...\n"); - err = gnupg_wait_process (runner->name, runner->pid, 1, &exitcode); - gnupg_release_process (runner->pid); - runner->pid = (pid_t)(-1); - if (err) + err = gnupg_process_wait (runner->proc, 1); + if (!err) + gnupg_process_ctl (runner->proc, GNUPG_PROCESS_GET_EXIT_ID, &exitcode); + gnupg_process_release (runner->proc); + runner->proc = NULL; + if (exitcode) log_error ("running '%s' failed (exitcode=%d): %s\n", runner->name, exitcode, gpg_strerror (err)); log_debug ("runner thread waiting finished\n"); } /* Get rid of the runner object (note: it is refcounted). */ log_debug ("runner thread releasing runner ...\n"); { runner_t r, rprev; for (r = running_threads, rprev = NULL; r; rprev = r, r = r->next_running) if (r == runner) { if (!rprev) running_threads = r->next_running; else rprev->next_running = r->next_running; r->next_running = NULL; break; } } runner_release (runner); log_debug ("runner thread runner released\n"); return NULL; } /* Spawn a new thread to let RUNNER work as a coprocess. */ gpg_error_t runner_spawn (runner_t runner) { gpg_error_t err; npth_attr_t tattr; npth_t thread; int ret; if (check_already_spawned (runner, "runner_spawn")) return gpg_error (GPG_ERR_BUG); /* In case we have an input fd, open it as an estream so that the Pth scheduling will work. The stdio functions don't work with Pth because they don't call the pth counterparts of read and write unless linker tricks are used. */ if (runner->in_fd != -1) { estream_t fp; fp = es_fdopen (runner->in_fd, "r"); if (!fp) { err = gpg_error_from_syserror (); log_error ("can't fdopen pipe for reading: %s\n", gpg_strerror (err)); return err; } runner->status_fp = fp; runner->in_fd = -1; /* Now owned by status_fp. */ } npth_attr_init (&tattr); npth_attr_setdetachstate (&tattr, NPTH_CREATE_DETACHED); ret = npth_create (&thread, &tattr, runner_thread, runner); if (ret) { err = gpg_error_from_errno (ret); log_error ("error spawning runner thread: %s\n", gpg_strerror (err)); return err; } npth_setname_np (thread, runner->name); /* The scheduler has not yet kicked in, thus we can safely set the spawned flag and the tid. */ runner->spawned = 1; runner->thread = thread; runner->next_running = running_threads; running_threads = runner; npth_attr_destroy (&tattr); /* The runner thread is now runnable. */ return 0; } /* Cancel a running thread. */ void runner_cancel (runner_t runner) { /* Warning: runner_cancel_all has knowledge of this code. */ if (runner->spawned) { runner->canceled = 1; /* Mark that we canceled this one already. */ /* FIXME: This does only work if the thread emits status lines. We need to change the thread to wait on an event. */ runner->cancel_flag = 1; /* For now we use the brutal way and kill the process. */ - gnupg_kill_process (runner->pid); + gnupg_process_terminate (runner->proc); } } /* Cancel all runner threads. */ void runner_cancel_all (void) { runner_t r; do { for (r = running_threads; r; r = r->next_running) if (r->spawned && !r->canceled) { runner_cancel (r); break; } } while (r); } /* Send a line of data down to the engine. This line may not contain a binary Nul or a LF character. This function is used by the engine's handler. */ gpg_error_t runner_send_line (runner_t runner, const void *data, size_t datalen) { gpg_error_t err = 0; if (!runner->spawned) { log_error ("BUG: runner for %s not spawned\n", runner->name); err = gpg_error (GPG_ERR_INTERNAL); } else if (runner->out_fd == -1) { log_error ("no output file descriptor for runner %s\n", runner->name); err = gpg_error (GPG_ERR_EBADF); } else if (data && datalen) { if (memchr (data, '\n', datalen)) { log_error ("LF detected in response data\n"); err = gpg_error (GPG_ERR_BUG); } else if (memchr (data, 0, datalen)) { log_error ("Nul detected in response data\n"); err = gpg_error (GPG_ERR_BUG); } else if (writen (runner->out_fd, data, datalen)) err = gpg_error_from_syserror (); } if (!err) if (writen (runner->out_fd, "\n", 1)) err = gpg_error_from_syserror (); return err; } diff --git a/g13/runner.h b/g13/runner.h index 36181adf9..01c395e02 100644 --- a/g13/runner.h +++ b/g13/runner.h @@ -1,76 +1,76 @@ /* runner.h - Run and watch the backend engines * Copyright (C) 2009 Free Software Foundation, Inc. * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #ifndef G13_RUNNER_H #define G13_RUNNER_H /* The runner object. */ struct runner_s; typedef struct runner_s *runner_t; /* Prototypes for the handler functions provided by the engine. */ typedef gpg_error_t (*engine_handler_fnc_t) (void *opaque, runner_t runner, const char *statusline); typedef void (*engine_handler_cleanup_fnc_t) (void *opaque); /* Return the number of active threads. */ unsigned int runner_get_threads (void); /* Create a new runner object. */ gpg_error_t runner_new (runner_t *r_runner, const char *name); /* Free a runner object. */ void runner_release (runner_t runner); /* Return the identifier of RUNNER. */ unsigned int runner_get_rid (runner_t runner); /* Find a runner by its rid. */ runner_t runner_find_by_rid (unsigned int rid); /* Functions to set properties of the runner. */ void runner_set_fds (runner_t runner, int in_fd, int out_fd); -void runner_set_pid (runner_t runner, pid_t pid); +void runner_set_proc (runner_t runner, gnupg_process_t proc); /* Register the handler functions with a runner. */ void runner_set_handler (runner_t runner, engine_handler_fnc_t handler, engine_handler_cleanup_fnc_t handler_cleanup, void *handler_data); /* Start the runner. */ gpg_error_t runner_spawn (runner_t runner); /* Cancel a runner. */ void runner_cancel (runner_t runner); /* Cancel all runner. */ void runner_cancel_all (void); /* Send data back to the engine. This function is used by the engine's handler. */ gpg_error_t runner_send_line (runner_t runner, const void *data, size_t datalen); #endif /*G13_RUNNER_H*/ diff --git a/scd/app.c b/scd/app.c index 3686c0f6c..468fed294 100644 --- a/scd/app.c +++ b/scd/app.c @@ -1,2724 +1,2735 @@ /* app.c - Application selection. * Copyright (C) 2003, 2004, 2005 Free Software Foundation, Inc. * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include #include #include #include #include #include #include "scdaemon.h" #include "../common/exechelp.h" #include "iso7816.h" #include "apdu.h" #include "../common/tlv.h" #include "../common/membuf.h" /* Forward declaration of internal function. */ static gpg_error_t select_additional_application_internal (card_t card, apptype_t req_apptype); static gpg_error_t send_serialno_and_app_status (card_t card, int with_apps, ctrl_t ctrl); static gpg_error_t run_reselect (ctrl_t ctrl, card_t c, app_t a, app_t a_prev); /* * Multiple readers, single writer (MRSW) lock. */ struct mrsw_lock { npth_mutex_t lock; npth_cond_t cond; int num_readers_active; int num_writers_waiting; int writer_active; npth_cond_t notify_cond; }; /* MRSW lock to protect the list of cards. * * This structure is used for serializing access to the list of cards * (by CARD_TOP). While allowing multiple accesses by different * connections as "r" access (for a CARD in the list), "w" access to * update the list is only possible with a single thread. * * Each use of a CARD (in the list) does "r" access. * * For "w" access, the app_send_devinfo function may wait on any * change of the list. For other cases of "w" access are opening new * card or removal of card, updating the list of card. * * Note that for serializing access to each CARD (and its associated * applications) itself, it is done separately by another mutex with * lock_card/unlock_card. */ static struct mrsw_lock card_list_lock; /* A list of card contexts. A card is a collection of applications * (described by app_t) on the same physical token. */ static card_t card_top; /* The list of application names and their select function. If no * specific application is selected the first available application on * a card is selected. */ struct app_priority_list_s { apptype_t apptype; char const *name; gpg_error_t (*select_func)(app_t); }; static struct app_priority_list_s app_priority_list[] = {{ APPTYPE_OPENPGP , "openpgp", app_select_openpgp }, { APPTYPE_PIV , "piv", app_select_piv }, { APPTYPE_NKS , "nks", app_select_nks }, { APPTYPE_P15 , "p15", app_select_p15 }, { APPTYPE_GELDKARTE, "geldkarte", app_select_geldkarte }, { APPTYPE_DINSIG , "dinsig", app_select_dinsig }, { APPTYPE_SC_HSM , "sc-hsm", app_select_sc_hsm }, { APPTYPE_NONE , NULL, NULL } /* APPTYPE_UNDEFINED is special and not listed here. */ }; /* Map a cardtype to a string. Never returns NULL. */ const char * strcardtype (cardtype_t t) { switch (t) { case CARDTYPE_GENERIC: return "generic"; case CARDTYPE_GNUK: return "gnuk"; case CARDTYPE_YUBIKEY: return "yubikey"; case CARDTYPE_ZEITCONTROL: return "zeitcontrol"; } return "?"; } /* Map an application type to a string. Never returns NULL. */ const char * strapptype (apptype_t t) { int i; for (i=0; app_priority_list[i].apptype; i++) if (app_priority_list[i].apptype == t) return app_priority_list[i].name; return t == APPTYPE_UNDEFINED? "undefined" : t? "?" : "none"; } const char * xstrapptype (app_t app) { return app? strapptype (app->apptype) : "[no_app]"; } /* Return the apptype for NAME. */ static apptype_t apptype_from_name (const char *name) { int i; if (!name) return APPTYPE_NONE; for (i=0; app_priority_list[i].apptype; i++) if (!ascii_strcasecmp (app_priority_list[i].name, name)) return app_priority_list[i].apptype; if (!ascii_strcasecmp ("undefined", name)) return APPTYPE_UNDEFINED; return APPTYPE_NONE; } /* Return the apptype for KEYREF. This is the first part of the * KEYREF up to the dot. */ static apptype_t apptype_from_keyref (const char *keyref) { int i; unsigned int n; const char *s; if (!keyref) return APPTYPE_NONE; s = strchr (keyref, '.'); if (!s || s == keyref || !s[1]) return APPTYPE_NONE; /* Not a valid keyref. */ n = s - keyref; for (i=0; app_priority_list[i].apptype; i++) if (strlen (app_priority_list[i].name) == n && !ascii_strncasecmp (app_priority_list[i].name, keyref, n)) return app_priority_list[i].apptype; return APPTYPE_NONE; } /* Return true if both serilanumbers are the same. This function * takes care of some peculiarities. */ static int is_same_serialno (const unsigned char *sna, size_t snalen, const unsigned char *snb, size_t snblen) { if ((!sna && !snb) || (!snalen && !snblen)) return 1; if (!sna || !snb) return 0; /* One of them is NULL. (Both NULL tested above). */ if (snalen != snblen) return 0; /* (No special cases for this below). */ /* The special case for OpenPGP cards where we ignore the version * bytes (vvvv). Example: D276000124010304000500009D8A0000 * ^^^^^^^^^^^^vvvvmmmmssssssssrrrr */ if (snalen == 16 && !memcmp (sna, "\xD2\x76\x00\x01\x24\x01", 6)) { if (memcmp (snb, "\xD2\x76\x00\x01\x24\x01", 6)) return 0; /* No */ return !memcmp (sna + 8, snb + 8, 8); } return !memcmp (sna, snb, snalen); } /* Initialization function to change the default app_priority_list. * LIST is a list of comma or space separated strings with application * names. Unknown names will only result in warning message. * Application not mentioned in LIST are used in their original order * after the given once. */ void app_update_priority_list (const char *arg) { struct app_priority_list_s save; char **names; int i, j, idx; names = strtokenize (arg, ", "); if (!names) log_fatal ("strtokenize failed: %s\n", gpg_strerror (gpg_error_from_syserror ())); idx = 0; for (i=0; names[i]; i++) { ascii_strlwr (names[i]); for (j=0; j < i; j++) if (!strcmp (names[j], names[i])) break; if (j < i) { log_info ("warning: duplicate application '%s' in priority list\n", names[i]); continue; } for (j=idx; app_priority_list[j].name; j++) if (!strcmp (names[i], app_priority_list[j].name)) break; if (!app_priority_list[j].name) { log_info ("warning: unknown application '%s' in priority list\n", names[i]); continue; } save = app_priority_list[idx]; app_priority_list[idx] = app_priority_list[j]; app_priority_list[j] = save; idx++; } log_assert (idx < DIM (app_priority_list)); xfree (names); for (i=0; app_priority_list[i].name; i++) log_info ("app priority %d: %s\n", i, app_priority_list[i].name); } static void print_progress_line (void *opaque, const char *what, int pc, int cur, int tot) { ctrl_t ctrl = opaque; char line[100]; if (ctrl) { snprintf (line, sizeof line, "%s %c %d %d", what, pc, cur, tot); send_status_direct (ctrl, "PROGRESS", line); } } /* Lock the CARD. This function shall be used right before calling * any of the actual application functions to serialize access to the * reader. We do this always even if the card is not actually used. * This allows an actual connection to assume that it never shares a * card (while performing one command). Returns 0 on success; only * then the unlock_reader function must be called after returning from * the handler. Right now we assume a that a reader has just one * card; this may eventually need refinement. */ static gpg_error_t lock_card (card_t card, ctrl_t ctrl) { if (npth_mutex_lock (&card->lock)) { gpg_error_t err = gpg_error_from_syserror (); log_error ("failed to acquire CARD lock for %p: %s\n", card, gpg_strerror (err)); return err; } apdu_set_progress_cb (card->slot, print_progress_line, ctrl); apdu_set_prompt_cb (card->slot, popup_prompt, ctrl); return 0; } /* Release a lock on a card. See lock_reader(). */ static void unlock_card (card_t card) { apdu_set_progress_cb (card->slot, NULL, NULL); apdu_set_prompt_cb (card->slot, NULL, NULL); if (npth_mutex_unlock (&card->lock)) { gpg_error_t err = gpg_error_from_syserror (); log_error ("failed to release CARD lock for %p: %s\n", card, gpg_strerror (err)); } } static void card_list_r_lock (void) { npth_mutex_lock (&card_list_lock.lock); while (card_list_lock.num_writers_waiting || card_list_lock.writer_active) npth_cond_wait (&card_list_lock.cond, &card_list_lock.lock); card_list_lock.num_readers_active++; npth_mutex_unlock (&card_list_lock.lock); } static void card_list_r_unlock (void) { npth_mutex_lock (&card_list_lock.lock); if (--card_list_lock.num_readers_active == 0) npth_cond_broadcast (&card_list_lock.cond); npth_mutex_unlock (&card_list_lock.lock); } static void card_list_w_lock (void) { npth_mutex_lock (&card_list_lock.lock); card_list_lock.num_writers_waiting++; while (card_list_lock.num_readers_active || card_list_lock.writer_active) npth_cond_wait (&card_list_lock.cond, &card_list_lock.lock); card_list_lock.num_writers_waiting--; card_list_lock.writer_active++; npth_mutex_unlock (&card_list_lock.lock); } static void card_list_w_unlock (void) { npth_mutex_lock (&card_list_lock.lock); card_list_lock.writer_active--; npth_cond_broadcast (&card_list_lock.cond); npth_mutex_unlock (&card_list_lock.lock); } static void card_list_signal (void) { npth_cond_broadcast (&card_list_lock.notify_cond); } static void card_list_wait (void) { npth_mutex_lock (&card_list_lock.lock); card_list_lock.writer_active--; npth_cond_broadcast (&card_list_lock.cond); npth_cond_wait (&card_list_lock.notify_cond, &card_list_lock.lock); card_list_lock.num_writers_waiting++; while (card_list_lock.num_readers_active || card_list_lock.writer_active) npth_cond_wait (&card_list_lock.cond, &card_list_lock.lock); card_list_lock.num_writers_waiting--; card_list_lock.writer_active++; npth_mutex_unlock (&card_list_lock.lock); } /* This function may be called to print information pertaining to the * current state of this module to the log. */ void app_dump_state (void) { card_t c; app_t a; card_list_r_lock (); for (c = card_top; c; c = c->next) { log_info ("app_dump_state: card=%p slot=%d type=%s refcount=%u\n", c, c->slot, strcardtype (c->cardtype), c->ref_count); /* FIXME The use of log_info risks a race! */ for (a=c->app; a; a = a->next) log_info ("app_dump_state: app=%p type='%s'\n", a, strapptype (a->apptype)); } card_list_r_unlock (); } /* * Send information for all available cards. * * With KEEP_LOOPING=0, it only outputs once. * With KEEP_LOOPING<0, it keeps looping, until it detects no device. * With KEEP_LOOPING>0, it keeps looping forever. */ gpg_error_t app_send_devinfo (ctrl_t ctrl, int keep_looping) { card_t c; app_t a; int no_device; card_list_w_lock (); while (1) { no_device = (card_top == NULL); if (no_device && keep_looping < 0) break; send_status_direct (ctrl, "DEVINFO_START", ""); for (c = card_top; c; c = c->next) { char *serialno; char card_info[80]; serialno = card_get_serialno (c); snprintf (card_info, sizeof card_info, "DEVICE %s %s", strcardtype (c->cardtype), serialno); xfree (serialno); for (a = c->app; a; a = a->next) send_status_direct (ctrl, card_info, strapptype (a->apptype)); } send_status_direct (ctrl, "DEVINFO_END", ""); if (keep_looping == 0) break; card_list_wait (); } card_list_w_unlock (); return no_device ? gpg_error (GPG_ERR_NOT_FOUND): 0; } /* Check whether the application NAME is allowed. This does not mean we have support for it though. */ static int is_app_allowed (const char *name) { strlist_t l; for (l=opt.disabled_applications; l; l = l->next) if (!strcmp (l->d, name)) return 0; /* no */ return 1; /* yes */ } /* This function is mainly used by the serialno command to check for * an application conflict which may appear if the serialno command is * used to request a specific application and the connection has * already done a select_application. Return values are: * 0 - No conflict * GPG_ERR_FALSE - Another application is in use but it is possible * to switch to the requested application. * Other code - Switching is not possible. * * If SERIALNO_BIN is not NULL a conflict is only asserted if the * serialno of the card matches. */ gpg_error_t check_application_conflict (card_t card, const char *name, const unsigned char *serialno_bin, size_t serialno_bin_len) { apptype_t apptype; if (!card || !name) return 0; if (!card->app) return gpg_error (GPG_ERR_CARD_NOT_INITIALIZED); /* Should not happen. */ if (serialno_bin && card->serialno) { if (!is_same_serialno (card->serialno, card->serialnolen, serialno_bin, serialno_bin_len)) return 0; /* The card does not match the requested S/N. */ } apptype = apptype_from_name (name); if (card->app->apptype == apptype) return 0; if (card->app->apptype == APPTYPE_UNDEFINED) return 0; if (card->cardtype == CARDTYPE_YUBIKEY) { if (card->app->apptype == APPTYPE_OPENPGP) { /* Current app is OpenPGP. */ if (!ascii_strcasecmp (name, "piv")) return gpg_error (GPG_ERR_FALSE); /* Switching allowed. */ } else if (card->app->apptype == APPTYPE_PIV) { /* Current app is PIV. */ if (!ascii_strcasecmp (name, "openpgp")) return gpg_error (GPG_ERR_FALSE); /* Switching allowed. */ } } log_info ("application '%s' in use - can't switch\n", strapptype (card->app->apptype)); return gpg_error (GPG_ERR_CONFLICT); } gpg_error_t card_reset (card_t card) { gpg_error_t err = 0; int sw; sw = apdu_reset (card->slot); if (sw) err = gpg_error (GPG_ERR_CARD_RESET); card->reset_requested = 1; scd_kick_the_loop (); gnupg_sleep (1); return err; } static gpg_error_t app_new_register (int slot, ctrl_t ctrl, const char *name, int periodical_check_needed) { gpg_error_t err = 0; card_t card = NULL; app_t app = NULL; unsigned char *result = NULL; size_t resultlen; int want_undefined; int i; /* Need to allocate a new card object */ card = xtrycalloc (1, sizeof *card); if (!card) { err = gpg_error_from_syserror (); log_info ("error allocating context: %s\n", gpg_strerror (err)); return err; } card->slot = slot; card->card_status = (unsigned int)-1; if (npth_mutex_init (&card->lock, NULL)) { err = gpg_error_from_syserror (); log_error ("error initializing mutex: %s\n", gpg_strerror (err)); xfree (card); return err; } err = lock_card (card, ctrl); if (err) { xfree (card); return err; } want_undefined = (name && !strcmp (name, "undefined")); /* Try to read the GDO file first to get a default serial number. We skip this if the undefined application has been requested. */ if (!want_undefined) { err = iso7816_select_file (slot, 0x3F00, 1); if (gpg_err_code (err) == GPG_ERR_CARD) { /* Might be SW==0x7D00. Let's test whether it is a Yubikey * by selecting its manager application and then reading the * config. */ static char const yk_aid[] = { 0xA0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17 }; /*MGR*/ static char const otp_aid[] = { 0xA0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01 }; /*OTP*/ unsigned char *buf; size_t buflen; const unsigned char *s0; unsigned char formfactor; size_t n; if (!iso7816_select_application (slot, yk_aid, sizeof yk_aid, 0x0001) && !iso7816_apdu_direct (slot, "\x00\x1d\x00\x00\x00", 5, 0, NULL, &buf, &buflen)) { card->cardtype = CARDTYPE_YUBIKEY; if (opt.verbose) { log_info ("Yubico: config="); log_printhex (buf, buflen, ""); } /* We skip the first byte which seems to be the total * length of the config data. */ if (buflen > 1) { s0 = find_tlv (buf+1, buflen-1, 0x04, &n); /* Form factor */ formfactor = (s0 && n == 1)? *s0 : 0; s0 = find_tlv (buf+1, buflen-1, 0x02, &n); /* Serial */ if (s0 && n <= 4) { card->serialno = xtrymalloc (3 + 1 + 4); if (card->serialno) { card->serialnolen = 3 + 1 + 4; card->serialno[0] = 0xff; card->serialno[1] = 0x02; card->serialno[2] = 0x0; card->serialno[3] = formfactor; memset (card->serialno + 4, 0, 4 - n); memcpy (card->serialno + 4 + 4 - n, s0, n); err = app_munge_serialno (card); } } s0 = find_tlv (buf+1, buflen-1, 0x05, &n); /* version */ if (s0 && n == 3) card->cardversion = ((s0[0]<<16)|(s0[1]<<8)|s0[2]); else if (!s0) { /* No version - this is not a Yubikey 5. We now * switch to the OTP app and take the first * three bytes of the response as version * number. */ xfree (buf); buf = NULL; if (!iso7816_select_application_ext (slot, otp_aid, sizeof otp_aid, 1, &buf, &buflen) && buflen > 3) card->cardversion = ((buf[0]<<16)|(buf[1]<<8)|buf[2]); } } xfree (buf); } } else { unsigned char *atr; size_t atrlen; /* This is heuristics to identify different implementations. */ atr = apdu_get_atr (slot, &atrlen); if (atr) { if (atrlen == 21 && atr[2] == 0x11) card->cardtype = CARDTYPE_GNUK; else if (atrlen == 21 && atr[7] == 0x75) card->cardtype = CARDTYPE_ZEITCONTROL; xfree (atr); } } if (!err && card->cardtype != CARDTYPE_YUBIKEY) err = iso7816_select_file (slot, 0x2F02, 0); if (!err && card->cardtype != CARDTYPE_YUBIKEY) err = iso7816_read_binary (slot, 0, 0, &result, &resultlen); if (!err && card->cardtype != CARDTYPE_YUBIKEY) { size_t n; const unsigned char *p; p = find_tlv_unchecked (result, resultlen, 0x5A, &n); if (p) resultlen -= (p-result); if (p && n > resultlen && n == 0x0d && resultlen+1 == n) { /* The object does not fit into the buffer. This is an invalid encoding (or the buffer is too short. However, I have some test cards with such an invalid encoding and therefore I use this ugly workaround to return something I can further experiment with. */ log_info ("enabling BMI testcard workaround\n"); n--; } if (p && n <= resultlen) { /* The GDO file is pretty short, thus we simply reuse it for storing the serial number. */ memmove (result, p, n); card->serialno = result; card->serialnolen = n; err = app_munge_serialno (card); if (err) goto leave; } else xfree (result); result = NULL; } } /* Allocate a new app object. */ app = xtrycalloc (1, sizeof *app); if (!app) { err = gpg_error_from_syserror (); log_info ("error allocating app context: %s\n", gpg_strerror (err)); goto leave; } card->app = app; app->card = card; /* Figure out the application to use. */ if (want_undefined) { /* We switch to the "undefined" application only if explicitly requested. */ app->apptype = APPTYPE_UNDEFINED; /* Clear the error so that we don't run through the application * selection chain. */ err = 0; } else { /* For certain error codes, there is no need to try more. */ if (gpg_err_code (err) == GPG_ERR_CARD_NOT_PRESENT || gpg_err_code (err) == GPG_ERR_ENODEV) goto leave; /* Set a default error so that we run through the application * selection chain. */ err = gpg_error (GPG_ERR_NOT_FOUND); } /* Find the first available app if NAME is NULL or the matching * NAME but only if that application is also enabled. */ for (i=0; err && app_priority_list[i].name; i++) { if (is_app_allowed (app_priority_list[i].name) && (!name || !strcmp (name, app_priority_list[i].name))) err = app_priority_list[i].select_func (app); } if (err && name && gpg_err_code (err) != GPG_ERR_OBJ_TERM_STATE) err = gpg_error (GPG_ERR_NOT_SUPPORTED); leave: if (err) { if (name) log_info ("can't select application '%s': %s\n", name, gpg_strerror (err)); else log_info ("no supported card application found: %s\n", gpg_strerror (err)); unlock_card (card); xfree (app); xfree (card); return err; } card->periodical_check_needed = periodical_check_needed; card->next = card_top; card_top = card; unlock_card (card); return 0; } /* If called with NAME as NULL, select the best fitting application * and return its card context; otherwise select the application with * NAME and return its card context. Returns an error code and stores * NULL at R_CARD if no application was found or no card is present. */ gpg_error_t select_application (ctrl_t ctrl, const char *name, int scan, const unsigned char *serialno_bin, size_t serialno_bin_len) { gpg_error_t err = 0; card_t card, card_prev = NULL; card_list_w_lock (); ctrl->card_ctx = NULL; if (scan || !card_top) { struct dev_list *l; int new_card = 0; /* Scan the devices to find new device(s). */ err = apdu_dev_list_start (opt.reader_port, &l); if (err) { card_list_w_unlock (); return err; } while (1) { int slot; int periodical_check_needed_this; slot = apdu_open_reader (l); if (slot < 0) break; periodical_check_needed_this = apdu_connect (slot); if (periodical_check_needed_this < 0) { /* We close a reader with no card. */ err = gpg_error (GPG_ERR_ENODEV); } else { err = app_new_register (slot, ctrl, name, periodical_check_needed_this); new_card++; } if (err) { pincache_put (ctrl, slot, NULL, NULL, NULL, 0); apdu_close_reader (slot); } } apdu_dev_list_finish (l); /* If new device(s), kick the scdaemon loop. */ if (new_card) scd_kick_the_loop (); } for (card = card_top; card; card = card->next) { lock_card (card, ctrl); if (serialno_bin == NULL) break; if (is_same_serialno (card->serialno, card->serialnolen, serialno_bin, serialno_bin_len)) break; unlock_card (card); card_prev = card; } if (card) { err = check_application_conflict (card, name, NULL, 0); if (!err) ctrl->current_apptype = card->app ? card->app->apptype : APPTYPE_NONE; else if (gpg_err_code (err) == GPG_ERR_FALSE) { apptype_t req_apptype = apptype_from_name (name); if (!req_apptype) err = gpg_error (GPG_ERR_NOT_FOUND); else { err = select_additional_application_internal (card, req_apptype); if (!err) ctrl->current_apptype = req_apptype; } } if (!err) { card->ref_count++; ctrl->card_ctx = card; if (card_prev) { card_prev->next = card->next; card->next = card_top; card_top = card; } } unlock_card (card); } else err = gpg_error (GPG_ERR_ENODEV); card_list_w_unlock (); return err; } /* Switch the current card for the session CTRL and print a SERIALNO * status line on success. (SERIALNO, SERIALNOLEN) is the binary s/n * of the card to switch to. */ gpg_error_t app_switch_current_card (ctrl_t ctrl, const unsigned char *serialno, size_t serialnolen) { gpg_error_t err; card_t card, cardtmp; card_list_r_lock (); cardtmp = ctrl->card_ctx; if (!cardtmp) { err = gpg_error (GPG_ERR_CARD_NOT_INITIALIZED); goto leave; } if (serialno && serialnolen) { for (card = card_top; card; card = card->next) { if (is_same_serialno (card->serialno, card->serialnolen, serialno, serialnolen)) break; } if (!card) { err = gpg_error (GPG_ERR_NOT_FOUND); goto leave; } /* Note: We do not lock CARD and CARDTMP here because we only * swap the context of the current session and there is no * chance of a context switch. This also works if the card * stays the same. */ ctrl->card_ctx = card; card->ref_count++; card_unref_locked (cardtmp); } /* Print the status line. */ err = send_serialno_and_app_status (ctrl->card_ctx, 0, ctrl); leave: card_list_r_unlock (); return err; } static gpg_error_t select_additional_application_internal (card_t card, apptype_t req_apptype) { gpg_error_t err = 0; app_t app; int i; /* Check that the requested app has not yet been put onto the list. */ for (app = card->app; app; app = app->next) if (app->apptype == req_apptype) { /* We already got this one. Note that in this case we don't * make it the current one but it doesn't matter because * maybe_switch_app will do that anyway. */ err = 0; app = NULL; goto leave; } /* Allocate a new app object. */ app = xtrycalloc (1, sizeof *app); if (!app) { err = gpg_error_from_syserror (); log_info ("error allocating app context: %s\n", gpg_strerror (err)); goto leave; } app->card = card; /* Find the app and run the select. */ for (i=0; app_priority_list[i].apptype; i++) { if (app_priority_list[i].apptype == req_apptype && is_app_allowed (app_priority_list[i].name)) { err = app_priority_list[i].select_func (app); break; } } if (!app_priority_list[i].apptype || (err && gpg_err_code (err) != GPG_ERR_OBJ_TERM_STATE)) err = gpg_error (GPG_ERR_NOT_SUPPORTED); if (err) goto leave; /* Add this app. We make it the current one to avoid an extra * reselect by maybe_switch_app after the select we just did. */ app->next = card->app; card->app = app; log_info ("added app '%s' to the card context and switched\n", strapptype (app->apptype)); leave: if (err) xfree (app); return err; } /* Add all possible additional applications to the card context but do * not change the current one. This currently works only for Yubikeys. */ static gpg_error_t select_all_additional_applications_internal (ctrl_t ctrl, card_t card) { gpg_error_t err = 0; apptype_t candidates[3]; int i, j; int any_new = 0; if (card->cardtype == CARDTYPE_YUBIKEY) { candidates[0] = APPTYPE_OPENPGP; candidates[1] = APPTYPE_PIV; candidates[2] = APPTYPE_NONE; } else { candidates[0] = APPTYPE_NONE; } /* Find the app and run the select. */ for (i=0; app_priority_list[i].apptype; i++) { app_t app, app_r, app_prev; for (j=0; candidates[j]; j++) if (candidates[j] == app_priority_list[i].apptype && is_app_allowed (app_priority_list[i].name)) break; if (!candidates[j]) continue; for (app = card->app; app; app = app->next) if (app->apptype == candidates[j]) break; if (app) continue; /* Already on the list of apps. */ app = xtrycalloc (1, sizeof *app); if (!app) { err = gpg_error_from_syserror (); log_info ("error allocating app context: %s\n", gpg_strerror (err)); goto leave; } app->card = card; err = app_priority_list[i].select_func (app); if (err) { log_error ("error selecting additional app '%s': %s - skipped\n", strapptype (candidates[j]), gpg_strerror (err)); err = 0; xfree (app); } else { /* Append to the list of apps. */ app_prev = card->app; for (app_r=app_prev->next; app_r; app_prev=app_r, app_r=app_r->next) ; app_prev->next = app; log_info ("added app '%s' to the card context\n", strapptype (app->apptype)); any_new = 1; } } /* If we found a new application we need to reselect the original * application so that we are in a well defined state. */ if (!err && any_new && card->app && card->app->fnc.reselect) err = run_reselect (ctrl, card, card->app, NULL); leave: return err; } /* This function needs to be called with the NAME of the new * application to be selected on CARD. On success the application is * added to the list of the card's active applications as currently * active application. On error no new application is allocated. * Selecting an already selected application has no effect. */ gpg_error_t select_additional_application (card_t card, ctrl_t ctrl, const char *name) { gpg_error_t err = 0; apptype_t req_apptype; if (!name) req_apptype = 0; else { req_apptype = apptype_from_name (name); if (!req_apptype) return gpg_error (GPG_ERR_NOT_FOUND); } if (req_apptype) { err = select_additional_application_internal (card, req_apptype); if (!err) { ctrl->current_apptype = req_apptype; if (DBG_APP) log_debug ("current_apptype is set to %s\n", name); } } else { err = select_all_additional_applications_internal (ctrl, card); } return err; } char * get_supported_applications (void) { int idx; size_t nbytes; char *buffer, *p; const char *s; for (nbytes=1, idx=0; (s=app_priority_list[idx].name); idx++) nbytes += strlen (s) + 1 + 1; buffer = xtrymalloc (nbytes); if (!buffer) return NULL; for (p=buffer, idx=0; (s=app_priority_list[idx].name); idx++) if (is_app_allowed (s)) p = stpcpy (stpcpy (p, s), ":\n"); *p = 0; return buffer; } /* Deallocate the application. */ static void deallocate_card (card_t card) { card_t c, c_prev = NULL; app_t a, anext; for (c = card_top; c; c = c->next) if (c == card) { if (c_prev == NULL) card_top = c->next; else c_prev->next = c->next; break; } else c_prev = c; if (card->ref_count) log_error ("releasing still used card context (%d)\n", card->ref_count); for (a = card->app; a; a = anext) { if (a->fnc.deinit) { a->fnc.deinit (a); a->fnc.deinit = NULL; } anext = a->next; xfree (a); } xfree (card->serialno); unlock_card (card); xfree (card); } static card_t do_with_keygrip (ctrl_t ctrl, int action, const char *keygrip_str, int capability) { int locked = 0; card_t c; app_t a, a_prev; for (c = card_top; c; c = c->next) { if (lock_card (c, ctrl)) { c = NULL; goto leave_the_loop; } locked = 1; a_prev = NULL; for (a = c->app; a; a = a->next) { if (!a->fnc.with_keygrip || a->need_reset) continue; /* Note that we need to do a re-select even for the current * app because the last selected application (e.g. after * init) might be a different one and we do not run * maybe_switch_app here. Of course we we do this only iff * we have an additional app. */ if (c->app->next) { if (run_reselect (ctrl, c, a, a_prev)) continue; } a_prev = a; if (DBG_APP) log_debug ("slot %d, app %s: calling with_keygrip(%s)\n", c->slot, xstrapptype (a), action == KEYGRIP_ACTION_SEND_DATA? "send_data": action == KEYGRIP_ACTION_WRITE_STATUS? "status": action == KEYGRIP_ACTION_LOOKUP? "lookup":"?"); if (!a->fnc.with_keygrip (a, ctrl, action, keygrip_str, capability)) goto leave_the_loop; /* ACTION_LOOKUP succeeded. */ } /* Select the first app again. */ if (c->app->next) run_reselect (ctrl, c, c->app, a_prev); unlock_card (c); locked = 0; } leave_the_loop: /* Force switching of the app if the selected one is not the current * one. Changing the current apptype is sufficient to do this. */ if (c && c->app && c->app->apptype != a->apptype) ctrl->current_apptype = a->apptype; if (locked && c) { unlock_card (c); locked = 0; } return c; } /* Locking access to the card-list and CARD, returns CARD. */ card_t card_get (ctrl_t ctrl, const char *keygrip) { card_t card; card_list_r_lock (); if (keygrip) card = do_with_keygrip (ctrl, KEYGRIP_ACTION_LOOKUP, keygrip, 0); else card = ctrl->card_ctx; if (!card) { card_list_r_unlock (); return NULL; } lock_card (card, NULL); return card; } /* Release the lock of CARD and the card-list. */ void card_put (card_t card) { /* We don't deallocate CARD here. Instead, we keep it. This is useful so that a card does not get reset even if only one session is using the card - this way the PIN cache and other cached data are preserved. */ unlock_card (card); card_list_r_unlock (); } /* This is the same as card_unref but assumes that CARD is already * locked. */ void card_unref_locked (card_t card) { if (!card) return; if (!card->ref_count) log_bug ("tried to release an already released card context\n"); --card->ref_count; } /* The serial number may need some cosmetics. Do it here. This function shall only be called once after a new serial number has been put into APP->serialno. Prefixes we use: FF 00 00 = For serial numbers starting with an FF FF 01 00 = Some german p15 cards return an empty serial number so the serial number from the EF(TokenInfo) is used instead. FF 02 00 = Serial number from Yubikey config. This is normally not seen because we modify this here to an OpenPGP Card s/n. FF 7F 00 = No serialno. All other serial numbers not starting with FF are used as they are. */ gpg_error_t app_munge_serialno (card_t card) { if (card->cardtype == CARDTYPE_YUBIKEY && card->serialnolen == 3 + 1 + 4 && !memcmp (card->serialno, "\xff\x02\x00", 3)) { /* An example for a serial number is * FF020001008A77C1 * ~~~~~~--~~~~~~~~ * ! ! !--------- 4 byte s/n * ! !----------- Form factor * !----------------- Our prefix * Yubico seems to use the decimalized version of their S/N * as the OpenPGP card S/N. Thus in theory we can contruct the * number from this information so that we do not rely on having * the OpenPGP app enabled. */ unsigned long sn; sn = card->serialno[4] * 16777216; sn += card->serialno[5] * 65536; sn += card->serialno[6] * 256; sn += card->serialno[7]; if (sn <= 99999999ul) { char *buf = xtrymalloc (16); if (!buf) return gpg_error_from_syserror (); memcpy (buf, "\xD2\x76\x00\x01\x24\x01", 6); buf[6] = 0; /* Application version which we don't know */ buf[7] = 0; /* thus we use 0.0 and don't use this directly. */ buf[8] = 0; /* Manufacturer: Yubico (0x0006). */ buf[9] = 6; buf[13] = (sn % 10); sn /= 10; buf[13] |= (sn % 10) << 4; sn /= 10; buf[12] = (sn % 10); sn /= 10; buf[12] |= (sn % 10) << 4; sn /= 10; buf[11] = (sn % 10); sn /= 10; buf[11] |= (sn % 10) << 4; sn /= 10; buf[10] = (sn % 10); sn /= 10; buf[10] |= (sn % 10) << 4; sn /= 10; buf[14] = 0; /* Last two bytes are RFU. */ buf[15] = 0; xfree (card->serialno); card->serialno = buf; card->serialnolen = 16; } } else if (card->serialnolen && card->serialno[0] == 0xff) { /* The serial number starts with our special prefix. This requires that we put our default prefix "FF0000" in front. */ unsigned char *p = xtrymalloc (card->serialnolen + 3); if (!p) return gpg_error_from_syserror (); memcpy (p, "\xff\0", 3); memcpy (p+3, card->serialno, card->serialnolen); card->serialnolen += 3; xfree (card->serialno); card->serialno = p; } else if (!card->serialnolen) { unsigned char *p = xtrymalloc (3); if (!p) return gpg_error_from_syserror (); memcpy (p, "\xff\x7f", 3); card->serialnolen = 3; xfree (card->serialno); card->serialno = p; } return 0; } /* Retrieve the serial number of the card. The serial number is returned as a malloced string (hex encoded) in SERIAL. Caller must free SERIAL unless the function returns an error. */ char * card_get_serialno (card_t card) { char *serial; if (!card) return NULL; if (!card->serialnolen) serial = xtrystrdup ("FF7F00"); else serial = bin2hex (card->serialno, card->serialnolen, NULL); return serial; } /* Same as card_get_serialno but takes an APP object. */ char * app_get_serialno (app_t app) { if (!app || !app->card) { gpg_err_set_errno (0); return NULL; } return card_get_serialno (app->card); } /* Return an allocated string with the serial number in a format to be * show to the user. With NOFALLBACK set to true return NULL if such an * abbreviated S/N is not available, else return the full serial * number as a hex string. May return NULL on malloc problem. */ char * card_get_dispserialno (card_t card, int nofallback) { char *result, *p; unsigned long sn; if (card && card->serialno && card->serialnolen == 3+1+4 && !memcmp (card->serialno, "\xff\x02\x00", 3)) { /* This is a 4 byte S/N of a Yubikey which seems to be printed * on the token in decimal. Maybe they will print larger S/N * also in decimal but we can't be sure, thus do it only for * these 32 bit numbers. */ sn = card->serialno[4] * 16777216; sn += card->serialno[5] * 65536; sn += card->serialno[6] * 256; sn += card->serialno[7]; if ((card->cardversion >> 16) >= 5) result = xtryasprintf ("%lu %03lu %03lu", (sn/1000000ul), (sn/1000ul % 1000ul), (sn % 1000ul)); else result = xtryasprintf ("%lu", sn); } else if (card && card->cardtype == CARDTYPE_YUBIKEY) { /* Get back the printed Yubikey number from the OpenPGP AID * Example: D2760001240100000006120808620000 */ result = card_get_serialno (card); if (result && strlen (result) >= 28 && !strncmp (result+16, "0006", 4)) { sn = atoi_4 (result+20) * 10000; sn += atoi_4 (result+24); if ((card->cardversion >> 16) >= 5) p = xtryasprintf ("%lu %03lu %03lu", (sn/1000000ul), (sn/1000ul % 1000ul), (sn % 1000ul)); else p = xtryasprintf ("%lu", sn); if (p) { xfree (result); result = p; } } else if (nofallback) { xfree (result); result = NULL; } } else if (card && card->app && card->app->apptype == APPTYPE_OPENPGP) { /* Extract number from standard OpenPGP AID. */ result = card_get_serialno (card); if (result && strlen (result) > 16+12) { memcpy (result, result+16, 4); result[4] = ' '; memcpy (result+5, result+20, 8); result[13] = 0; } else if (nofallback) { xfree (result); result = NULL; } } else if (nofallback) result = NULL; /* No Abbreviated S/N. */ else result = card_get_serialno (card); return result; } /* Same as card_get_dispserialno but takes an APP object. */ char * app_get_dispserialno (app_t app, int nofallback) { if (!app || !app->card) { gpg_err_set_errno (0); return NULL; } return card_get_dispserialno (app->card, nofallback); } /* Helper to run the reselect function. */ static gpg_error_t run_reselect (ctrl_t ctrl, card_t c, app_t a, app_t a_prev) { gpg_error_t err; if (!a->fnc.reselect) { log_info ("slot %d, app %s: re-select not implemented\n", c->slot, xstrapptype (a)); return gpg_error (GPG_ERR_CARD_NOT_INITIALIZED); } /* Give the current app a chance to save some state before another * app is selected. We ignore errors here because that state saving * (e.g. putting PINs into a cache) is a convenience feature and not * required to always work. */ if (a_prev && a_prev->fnc.prep_reselect) { if (a_prev->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = a_prev->fnc.prep_reselect (a_prev, ctrl); if (err) log_error ("slot %d, app %s: preparing re-select from %s failed: %s\n", c->slot, xstrapptype (a), xstrapptype (a_prev), gpg_strerror (err)); } if (a->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = a->fnc.reselect (a, ctrl); if (err) { log_error ("slot %d, app %s: error re-selecting: %s\n", c->slot, xstrapptype (a), gpg_strerror (err)); return err; } if (DBG_APP) log_debug ("slot %d, app %s: re-selected\n", c->slot, xstrapptype (a)); return 0; } /* * Check external interference before each use of the application on * card. Returns -1 when detecting some external interference. * Returns 0 if not. * * Note: This kind of detection can't be perfect. At most, it may be * possibly useful kludge, in some limited situations. */ static int check_external_interference (app_t app, ctrl_t ctrl) { /* * Only when a user is using Yubikey with pcsc-shared configuration, * we need this detection. Otherwise, the card/token is under full * control of scdaemon, there's no problem at all. However, if the * APDU command has been used we better also check whether the AID * is still valid. */ if (app && app->card && app->card->maybe_check_aid) app->card->maybe_check_aid = 0; else if (!opt.pcsc_shared || app->card->cardtype != CARDTYPE_YUBIKEY) return 0; if (app->fnc.check_aid) { unsigned char *aid; size_t aidlen; gpg_error_t err; int slot = app_get_slot (app); err = iso7816_get_data (slot, 0, 0x004F, &aid, &aidlen); if (err) return -1; err = app->fnc.check_aid (app, ctrl, aid, aidlen); xfree (aid); if (err) return -1; } return 0; } /* Check that the card has been initialized and whether we need to * switch to another application on the same card. Switching means * that the new active app will be moved to the head of the list at * CARD->app. This function must be called with the card lock held. */ static gpg_error_t maybe_switch_app (ctrl_t ctrl, card_t card, const char *keyref) { gpg_error_t err; app_t app; app_t app_prev = NULL; apptype_t apptype; if (!card->app) return gpg_error (GPG_ERR_CARD_NOT_INITIALIZED); if (card->maybe_check_aid && card->app->fnc.reselect && check_external_interference (card->app, ctrl)) { if (DBG_APP) log_debug ("slot %d, app %s: forced re-select due to direct APDU use\n", card->slot, xstrapptype (card->app)); err = card->app->fnc.reselect (card->app, ctrl); if (err) log_error ("slot %d, app %s: forced re-select failed: %s - ignored\n", card->slot, xstrapptype (card->app), gpg_strerror (err)); err = 0; } if (!ctrl->current_apptype) { /* For whatever reasons the current apptype has not been set - * fix that and use the current app. */ if (DBG_APP) log_debug ("slot %d: no current app switching to %s\n", card->slot, strapptype (card->app->apptype)); ctrl->current_apptype = card->app->apptype; return 0; } for (app = card->app; app; app = app->next) if (app->apptype == ctrl->current_apptype) break; if (!app) { /* The current app is not supported by this card. Set the first * app of the card as current. */ if (DBG_APP) log_debug ("slot %d: current app %s not available switching to %s\n", card->slot, strapptype (ctrl->current_apptype), strapptype (card->app->apptype)); ctrl->current_apptype = card->app->apptype; return 0; } if (DBG_APP) log_debug ("slot %d: have=%s want=%s keyref=%s\n", card->slot, strapptype (card->app->apptype), strapptype (ctrl->current_apptype), keyref? keyref:"[none]"); app = NULL; if (keyref) { /* Switch based on the requested KEYREF. */ apptype = apptype_from_keyref (keyref); if (apptype) { for (app = card->app; app; app_prev = app, app = app->next) if (app->apptype == apptype) break; if (!app_prev && ctrl->current_apptype == card->app->apptype) if (check_external_interference (app, ctrl) == 0) return 0; /* Already the first app - no need to switch. */ } else if (strlen (keyref) == 40) { /* This looks like a keygrip. Iterate over all apps to find * the corresponding app. */ for (app = card->app; app; app_prev = app, app = app->next) if (app->fnc.with_keygrip && !app->need_reset && !app->fnc.with_keygrip (app, ctrl, KEYGRIP_ACTION_LOOKUP, keyref, 0)) break; if (!app_prev && ctrl->current_apptype == card->app->apptype) if (check_external_interference (app, ctrl) == 0) return 0; /* Already the first app - no need to switch. */ } } if (!app) { /* Switch based on the current application of this connection or * if a keyref based switch didn't worked. */ if (ctrl->current_apptype == card->app->apptype) return 0; /* No need to switch. */ app_prev = card->app; for (app = app_prev->next; app; app_prev = app, app = app->next) if (app->apptype == ctrl->current_apptype) break; } if (!app) return gpg_error (GPG_ERR_WRONG_CARD); err = run_reselect (ctrl, card, app, app_prev); if (err) return err; /* Swap APP with the head of the app list if needed. Note that APP * is not the head of the list. */ if (app_prev) { app_prev->next = app->next; app->next = card->app; card->app = app; } if (opt.verbose) log_info ("slot %d, app %s: %s\n", card->slot, xstrapptype (app), app_prev? "switched":"re-selected"); ctrl->current_apptype = app->apptype; return 0; } /* Helper for app_write_learn_status. */ static gpg_error_t write_learn_status_core (card_t card, app_t app, ctrl_t ctrl, unsigned int flags) { gpg_error_t err; /* We do not send CARD and APPTYPE if only keypairinfo is requested. */ if (!(flags & APP_LEARN_FLAG_KEYPAIRINFO)) { if (card && card->cardtype) send_status_direct (ctrl, "CARDTYPE", strcardtype (card->cardtype)); if (card && card->cardversion) send_status_printf (ctrl, "CARDVERSION", "%X", card->cardversion); if (app->apptype) send_status_direct (ctrl, "APPTYPE", strapptype (app->apptype)); if (app->appversion) send_status_printf (ctrl, "APPVERSION", "%X", app->appversion); } if (app->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else { err = app->fnc.learn_status (app, ctrl, flags); if (err && (flags & APP_LEARN_FLAG_REREAD)) app->need_reset = 1; } return err; } /* Write out the application specific status lines for the LEARN command. */ gpg_error_t app_write_learn_status (card_t card, ctrl_t ctrl, unsigned int flags) { gpg_error_t err, err2, tmperr; app_t app, last_app; int any_reselect = 0; /* Always make sure that the current app for this connection has * been selected and is at the top of the list. */ if ((err = maybe_switch_app (ctrl, card, NULL))) ; else if (!card->app->fnc.learn_status) err = gpg_error (GPG_ERR_UNSUPPORTED_OPERATION); else { err = write_learn_status_core (card, card->app, ctrl, flags); if (!err && card->app->fnc.reselect && (flags & APP_LEARN_FLAG_MULTI)) { /* The current app has the reselect feature so that we can * loop over all other apps which are capable of a reselect * and finally reselect the first app again. Note that we * did the learn for the currently selected card above. */ app = last_app = card->app; for (app = app->next; app && !err; app = app->next) if (app->fnc.reselect) { if (last_app && last_app->fnc.prep_reselect) { tmperr = last_app->fnc.prep_reselect (last_app, ctrl); if (tmperr) log_info ("slot %d, app %s:" " preparing re-select from %s failed: %s\n", card->slot, xstrapptype (app), xstrapptype (last_app), gpg_strerror (tmperr)); } any_reselect = 1; err = app->fnc.reselect (app, ctrl); if (!err) { last_app = app; err = write_learn_status_core (NULL, app, ctrl, flags); } } app = card->app; if (any_reselect) { if (last_app && last_app->fnc.prep_reselect) { tmperr = last_app->fnc.prep_reselect (last_app, ctrl); if (tmperr) log_info ("slot %d, app %s:" " preparing re-select from %s failed: %s\n", card->slot, xstrapptype (app), xstrapptype (last_app), gpg_strerror (tmperr)); } err2 = app->fnc.reselect (app, ctrl); if (err2) { log_error ("error re-selecting '%s': %s\n", strapptype(app->apptype), gpg_strerror (err2)); if (!err) err = err2; } } } } return err; } /* Read the certificate with id CERTID (as returned by learn_status in the CERTINFO status lines) and return it in the freshly allocated buffer put into CERT and the length of the certificate put into CERTLEN. */ gpg_error_t app_readcert (card_t card, ctrl_t ctrl, const char *certid, unsigned char **cert, size_t *certlen) { gpg_error_t err; if ((err = maybe_switch_app (ctrl, card, certid))) ; else if (!card->app->fnc.readcert) err = gpg_error (GPG_ERR_UNSUPPORTED_OPERATION); else { if (DBG_APP) log_debug ("slot %d app %s: calling readcert(%s)\n", card->slot, xstrapptype (card->app), certid); if (card->app->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = card->app->fnc.readcert (card->app, certid, cert, certlen); } return err; } /* Read the key with ID KEYID. On success a canonical encoded * S-expression with the public key will get stored at PK and its * length (for assertions) at PKLEN; the caller must release that * buffer. On error NULL will be stored at PK and PKLEN and an error * code returned. If the key is not required NULL may be passed for * PK; this makes sense if the APP_READKEY_FLAG_INFO has also been set. * * This function might not be supported by all applications. */ gpg_error_t app_readkey (card_t card, ctrl_t ctrl, const char *keyid, unsigned int flags, unsigned char **pk, size_t *pklen) { gpg_error_t err; if (pk) *pk = NULL; if (pklen) *pklen = 0; if (!keyid) return gpg_error (GPG_ERR_INV_VALUE); if ((err = maybe_switch_app (ctrl, card, keyid))) ; else if (!card->app->fnc.readkey) err = gpg_error (GPG_ERR_UNSUPPORTED_OPERATION); else { if (DBG_APP) log_debug ("slot %d app %s: calling readkey(%s)\n", card->slot, xstrapptype (card->app), keyid); if (card->app->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = card->app->fnc.readkey (card->app, ctrl, keyid, flags, pk, pklen); } return err; } /* Perform a GETATTR operation. */ gpg_error_t app_getattr (card_t card, ctrl_t ctrl, const char *name) { gpg_error_t err; if (!name || !*name) return gpg_error (GPG_ERR_INV_VALUE); if ((err = maybe_switch_app (ctrl, card, NULL))) ; else if (name && !strcmp (name, "CARDTYPE")) { send_status_direct (ctrl, "CARDTYPE", strcardtype (card->cardtype)); } else if (name && !strcmp (name, "APPTYPE")) { send_status_direct (ctrl, "APPTYPE", strapptype (card->app->apptype)); } else if (name && !strcmp (name, "SERIALNO")) { char *serial; serial = app_get_serialno (card->app); if (!serial) err = gpg_error (GPG_ERR_INV_VALUE); else { send_status_direct (ctrl, "SERIALNO", serial); xfree (serial); } } else if (!card->app->fnc.getattr) err = gpg_error (GPG_ERR_UNSUPPORTED_OPERATION); else { if (DBG_APP) log_debug ("slot %d app %s: calling getattr(%s)\n", card->slot, xstrapptype (card->app), name); if (card->app->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = card->app->fnc.getattr (card->app, ctrl, name); } return err; } /* Perform a SETATTR operation. */ gpg_error_t app_setattr (card_t card, ctrl_t ctrl, const char *name, gpg_error_t (*pincb)(void*, const char *, char **), void *pincb_arg, const unsigned char *value, size_t valuelen) { gpg_error_t err; if (!name || !*name || !value) return gpg_error (GPG_ERR_INV_VALUE); if ((err = maybe_switch_app (ctrl, card, NULL))) ; else if (!card->app->fnc.setattr) err = gpg_error (GPG_ERR_UNSUPPORTED_OPERATION); else { if (DBG_APP) log_debug ("slot %d app %s: calling setattr(%s)\n", card->slot, xstrapptype (card->app), name); if (card->app->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = card->app->fnc.setattr (card->app, ctrl, name, pincb, pincb_arg, value, valuelen); } return err; } /* Create the signature and return the allocated result in OUTDATA. If a PIN is required the PINCB will be used to ask for the PIN; it should return the PIN in an allocated buffer and put it into PIN. */ gpg_error_t app_sign (card_t card, ctrl_t ctrl, const char *keyidstr, int hashalgo, gpg_error_t (*pincb)(void*, const char *, char **), void *pincb_arg, const void *indata, size_t indatalen, unsigned char **outdata, size_t *outdatalen ) { gpg_error_t err; if (!indata || !indatalen || !outdata || !outdatalen || !pincb) return gpg_error (GPG_ERR_INV_VALUE); if ((err = maybe_switch_app (ctrl, card, keyidstr))) ; else if (!card->app->fnc.sign) err = gpg_error (GPG_ERR_UNSUPPORTED_OPERATION); else { if (DBG_APP) log_debug ("slot %d app %s: calling sign(%s)\n", card->slot, xstrapptype (card->app), keyidstr); if (card->app->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = card->app->fnc.sign (card->app, ctrl, keyidstr, hashalgo, pincb, pincb_arg, indata, indatalen, outdata, outdatalen); } if (opt.verbose) log_info ("operation sign result: %s\n", gpg_strerror (err)); return err; } /* Create the signature using the INTERNAL AUTHENTICATE command and return the allocated result in OUTDATA. If a PIN is required the PINCB will be used to ask for the PIN; it should return the PIN in an allocated buffer and put it into PIN. */ gpg_error_t app_auth (card_t card, ctrl_t ctrl, const char *keyidstr, gpg_error_t (*pincb)(void*, const char *, char **), void *pincb_arg, const void *indata, size_t indatalen, unsigned char **outdata, size_t *outdatalen ) { gpg_error_t err; if (!outdata || !outdatalen || !pincb) return gpg_error (GPG_ERR_INV_VALUE); if ((err = maybe_switch_app (ctrl, card, keyidstr))) ; else if (!card->app->fnc.auth) err = gpg_error (GPG_ERR_UNSUPPORTED_OPERATION); else { if (card->app->apptype != APPTYPE_OPENPGP && (!indata || !indatalen)) return gpg_error (GPG_ERR_INV_VALUE); if (DBG_APP) log_debug ("slot %d app %s: calling auth(%s)\n", card->slot, xstrapptype (card->app), keyidstr); if (card->app->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = card->app->fnc.auth (card->app, ctrl, keyidstr, pincb, pincb_arg, indata, indatalen, outdata, outdatalen); } if (opt.verbose) log_info ("operation auth result: %s\n", gpg_strerror (err)); return err; } /* Decrypt the data in INDATA and return the allocated result in OUTDATA. If a PIN is required the PINCB will be used to ask for the PIN; it should return the PIN in an allocated buffer and put it into PIN. */ gpg_error_t app_decipher (card_t card, ctrl_t ctrl, const char *keyidstr, gpg_error_t (*pincb)(void*, const char *, char **), void *pincb_arg, const void *indata, size_t indatalen, unsigned char **outdata, size_t *outdatalen, unsigned int *r_info) { gpg_error_t err; *r_info = 0; if (!indata || !indatalen || !outdata || !outdatalen || !pincb) return gpg_error (GPG_ERR_INV_VALUE); if ((err = maybe_switch_app (ctrl, card, keyidstr))) ; else if (!card->app->fnc.decipher) err = gpg_error (GPG_ERR_UNSUPPORTED_OPERATION); else { if (DBG_APP) log_debug ("slot %d app %s: calling decipher(%s)\n", card->slot, xstrapptype (card->app), keyidstr); if (card->app->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = card->app->fnc.decipher (card->app, ctrl, keyidstr, pincb, pincb_arg, indata, indatalen, outdata, outdatalen, r_info); } if (opt.verbose) log_info ("operation decipher result: %s\n", gpg_strerror (err)); return err; } /* Perform the WRITECERT operation. */ gpg_error_t app_writecert (card_t card, ctrl_t ctrl, const char *certidstr, gpg_error_t (*pincb)(void*, const char *, char **), void *pincb_arg, const unsigned char *data, size_t datalen) { gpg_error_t err; if (!certidstr || !*certidstr || !pincb) return gpg_error (GPG_ERR_INV_VALUE); if ((err = maybe_switch_app (ctrl, card, certidstr))) ; else if (!card->app->fnc.writecert) err = gpg_error (GPG_ERR_UNSUPPORTED_OPERATION); else { if (DBG_APP) log_debug ("slot %d app %s: calling writecert(%s)\n", card->slot, xstrapptype (card->app), certidstr); if (card->app->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = card->app->fnc.writecert (card->app, ctrl, certidstr, pincb, pincb_arg, data, datalen); } if (opt.verbose) log_info ("operation writecert result: %s\n", gpg_strerror (err)); return err; } /* Perform the WRITEKEY operation. */ gpg_error_t app_writekey (card_t card, ctrl_t ctrl, const char *keyidstr, unsigned int flags, gpg_error_t (*pincb)(void*, const char *, char **), void *pincb_arg, const unsigned char *keydata, size_t keydatalen) { gpg_error_t err; if (!keyidstr || !*keyidstr || !pincb) return gpg_error (GPG_ERR_INV_VALUE); if ((err = maybe_switch_app (ctrl, card, keyidstr))) ; else if (!card->app->fnc.writekey) err = gpg_error (GPG_ERR_UNSUPPORTED_OPERATION); else { if (DBG_APP) log_debug ("slot %d app %s: calling writekey(%s)\n", card->slot, xstrapptype (card->app), keyidstr); if (card->app->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = card->app->fnc.writekey (card->app, ctrl, keyidstr, flags, pincb, pincb_arg, keydata, keydatalen); } if (opt.verbose) log_info ("operation writekey result: %s\n", gpg_strerror (err)); return err; } /* Perform a GENKEY operation. */ gpg_error_t app_genkey (card_t card, ctrl_t ctrl, const char *keynostr, const char *keytype, unsigned int flags, time_t createtime, gpg_error_t (*pincb)(void*, const char *, char **), void *pincb_arg) { gpg_error_t err; if (!keynostr || !*keynostr || !pincb) return gpg_error (GPG_ERR_INV_VALUE); if ((err = maybe_switch_app (ctrl, card, keynostr))) ; else if (!card->app->fnc.genkey) err = gpg_error (GPG_ERR_UNSUPPORTED_OPERATION); else { if (DBG_APP) log_debug ("slot %d app %s: calling genkey(%s)\n", card->slot, xstrapptype (card->app), keynostr); if (card->app->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = card->app->fnc.genkey (card->app, ctrl, keynostr, keytype, flags, createtime, pincb, pincb_arg); } if (opt.verbose) log_info ("operation genkey result: %s\n", gpg_strerror (err)); return err; } /* Perform a GET CHALLENGE operation. This function is special as it directly accesses the card without any application specific wrapper. */ gpg_error_t app_get_challenge (card_t card, ctrl_t ctrl, size_t nbytes, unsigned char *buffer) { (void)ctrl; if (!nbytes || !buffer) return gpg_error (GPG_ERR_INV_VALUE); return iso7816_get_challenge (card->slot, nbytes, buffer); } /* Perform a CHANGE REFERENCE DATA or RESET RETRY COUNTER operation. */ gpg_error_t app_change_pin (card_t card, ctrl_t ctrl, const char *chvnostr, unsigned int flags, gpg_error_t (*pincb)(void*, const char *, char **), void *pincb_arg) { gpg_error_t err; if (!chvnostr || !*chvnostr || !pincb) return gpg_error (GPG_ERR_INV_VALUE); if ((err = maybe_switch_app (ctrl, card, NULL))) ; else if (!card->app->fnc.change_pin) err = gpg_error (GPG_ERR_UNSUPPORTED_OPERATION); else { if (DBG_APP) log_debug ("slot %d app %s: calling change_pin(%s)\n", card->slot, xstrapptype (card->app), chvnostr); if (card->app->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = card->app->fnc.change_pin (card->app, ctrl, chvnostr, flags, pincb, pincb_arg); } if (opt.verbose) log_info ("operation change_pin result: %s\n", gpg_strerror (err)); return err; } /* Perform a VERIFY operation without doing anything else. This may be used to initialize a the PIN cache for long lasting other operations. Its use is highly application dependent. */ gpg_error_t app_check_pin (card_t card, ctrl_t ctrl, const char *keyidstr, gpg_error_t (*pincb)(void*, const char *, char **), void *pincb_arg) { gpg_error_t err; if (!keyidstr || !*keyidstr || !pincb) return gpg_error (GPG_ERR_INV_VALUE); if ((err = maybe_switch_app (ctrl, card, NULL))) ; else if (!card->app->fnc.check_pin) err = gpg_error (GPG_ERR_UNSUPPORTED_OPERATION); else { if (DBG_APP) log_debug ("slot %d app %s: calling check_pin(%s)\n", card->slot, xstrapptype (card->app), keyidstr); if (card->app->need_reset) err = gpg_error (GPG_ERR_CARD_RESET); else err = card->app->fnc.check_pin (card->app, ctrl, keyidstr, pincb, pincb_arg); } if (opt.verbose) log_info ("operation check_pin result: %s\n", gpg_strerror (err)); return err; } +static void +setup_env (struct spawn_cb_arg *sca) +{ +#ifdef HAVE_W32_SYSTEM + (void)sca; /* Not supported on Windows. */ +#else + char *v = sca->arg; + + putenv (v); +#endif +} + static void report_change (int slot, int old_status, int cur_status) { char *homestr, *envstr; char *fname; char templ[50]; estream_t fp; snprintf (templ, sizeof templ, "reader_%d.status", slot); fname = make_filename (gnupg_homedir (), templ, NULL ); fp = es_fopen (fname, "w"); if (fp) { es_fprintf (fp, "%s\n", (cur_status & 1)? "USABLE": (cur_status & 4)? "ACTIVE": (cur_status & 2)? "PRESENT": "NOCARD"); es_fclose (fp); } xfree (fname); homestr = make_filename (gnupg_homedir (), NULL); if (gpgrt_asprintf (&envstr, "GNUPGHOME=%s", homestr) < 0) log_error ("out of core while building environment\n"); else { gpg_error_t err; - const char *args[9], *envs[2]; + const char *args[9]; char numbuf1[30], numbuf2[30], numbuf3[30]; - envs[0] = envstr; - envs[1] = NULL; - sprintf (numbuf1, "%d", slot); sprintf (numbuf2, "0x%04X", old_status); sprintf (numbuf3, "0x%04X", cur_status); args[0] = "--reader-port"; args[1] = numbuf1; args[2] = "--old-code"; args[3] = numbuf2; args[4] = "--new-code"; args[5] = numbuf3; args[6] = "--status"; args[7] = ((cur_status & 1)? "USABLE": (cur_status & 4)? "ACTIVE": (cur_status & 2)? "PRESENT": "NOCARD"); args[8] = NULL; fname = make_filename (gnupg_homedir (), "scd-event", NULL); - err = gnupg_spawn_process_detached (fname, args, envs); + err = gnupg_process_spawn (fname, args, + GNUPG_PROCESS_DETACHED, + setup_env, envstr, NULL); if (err && gpg_err_code (err) != GPG_ERR_ENOENT) log_error ("failed to run event handler '%s': %s\n", fname, gpg_strerror (err)); xfree (fname); xfree (envstr); } xfree (homestr); } int scd_update_reader_status_file (void) { card_t card, card_next; int periodical_check_needed = 0; int reported = 0; card_list_w_lock (); for (card = card_top; card; card = card_next) { int sw; unsigned int status; lock_card (card, NULL); card_next = card->next; if (card->reset_requested) { /* Here is the post-processing of RESET request. */ status = 0; card->reset_requested = 0; } else { sw = apdu_get_status (card->slot, 0, &status); if (sw == SW_HOST_NO_READER) { /* Most likely the _reader_ has been unplugged. */ status = 0; } else if (sw) { /* Get status failed. Ignore that. */ if (card->periodical_check_needed) periodical_check_needed = 1; unlock_card (card); continue; } } if (card->card_status != status) { report_change (card->slot, card->card_status, status); send_client_notifications (card, status == 0); reported++; if (status == 0) { if (DBG_APP) log_debug ("Removal of a card: %d\n", card->slot); pincache_put (NULL, card->slot, NULL, NULL, NULL, 0); apdu_close_reader (card->slot); deallocate_card (card); } else { card->card_status = status; if (card->periodical_check_needed) periodical_check_needed = 1; unlock_card (card); } } else { if (card->periodical_check_needed) periodical_check_needed = 1; unlock_card (card); } } if (reported) card_list_signal (); card_list_w_unlock (); return periodical_check_needed; } /* This function must be called once to initialize this module. This has to be done before a second thread is spawned. We can't do the static initialization because Pth emulation code might not be able to do a static init; in particular, it is not possible for W32. */ gpg_error_t initialize_module_command (void) { gpg_error_t err; card_list_lock.num_readers_active = 0; card_list_lock.num_writers_waiting = 0; card_list_lock.writer_active = 0; if (npth_mutex_init (&card_list_lock.lock, NULL)) { err = gpg_error_from_syserror (); log_error ("app: error initializing mutex: %s\n", gpg_strerror (err)); return err; } err = npth_cond_init (&card_list_lock.cond, NULL); if (err) { err = gpg_error_from_syserror (); log_error ("npth_cond_init failed: %s\n", gpg_strerror (err)); return err; } err = npth_cond_init (&card_list_lock.notify_cond, NULL); if (err) { err = gpg_error_from_syserror (); log_error ("npth_cond_init failed: %s\n", gpg_strerror (err)); return err; } return apdu_init (); } /* Sort helper for app_send_card_list. */ static int compare_card_list_items (const void *arg_a, const void *arg_b) { const card_t a = *(const card_t *)arg_a; const card_t b = *(const card_t *)arg_b; return a->slot - b->slot; } /* Helper for send_card_and_app_list and app_switch_active_app. */ static gpg_error_t send_serialno_and_app_status (card_t card, int with_apps, ctrl_t ctrl) { gpg_error_t err; app_t a; char *serial; char *p; membuf_t mb; int any = 0; serial = card_get_serialno (card); if (!serial) return 0; /* Oops. */ if (with_apps) { /* Note that in case the additional applications have not yet been * added to the card context (which is commonly done by means of * "SERIALNO --all", we do that here. */ err = select_all_additional_applications_internal (ctrl, card); if (err) { xfree (serial); return err; } init_membuf (&mb, 256); put_membuf_str (&mb, serial); for (a = card->app; a; a = a->next) { if (!a->fnc.with_keygrip || a->need_reset) continue; any = 1; put_membuf (&mb, " ", 1); put_membuf_str (&mb, xstrapptype (a)); } if (!any && card->app) { /* No card app supports the with_keygrip function. Use the * main app as fallback. */ put_membuf (&mb, " ", 1); put_membuf_str (&mb, xstrapptype (card->app)); } put_membuf (&mb, "", 1); p = get_membuf (&mb, NULL); if (!p) { err = gpg_error_from_syserror (); xfree (serial); return err; } send_status_direct (ctrl, "SERIALNO", p); xfree (p); } else send_status_direct (ctrl, "SERIALNO", serial); xfree (serial); return 0; } /* Common code for app_send_card_list and app_send_active_apps. */ static gpg_error_t send_card_and_app_list (ctrl_t ctrl, card_t wantcard, int with_apps) { gpg_error_t err; card_t c; card_t *cardlist = NULL; int n, ncardlist; card_list_r_lock (); for (n=0, c = card_top; c; c = c->next) n++; if (!n) { err = gpg_error (GPG_ERR_CARD_NOT_PRESENT); goto leave; } cardlist = xtrycalloc (n, sizeof *cardlist); if (!cardlist) { err = gpg_error_from_syserror (); goto leave; } for (ncardlist=0, c = card_top; c; c = c->next) cardlist[ncardlist++] = c; qsort (cardlist, ncardlist, sizeof *cardlist, compare_card_list_items); for (n=0; n < ncardlist; n++) { if (wantcard && wantcard != cardlist[n]) continue; err = send_serialno_and_app_status (cardlist[n], with_apps, ctrl); if (err) goto leave; } err = 0; leave: card_list_r_unlock (); xfree (cardlist); return err; } /* Send status lines with the serialno of all inserted cards. */ gpg_error_t app_send_card_list (ctrl_t ctrl) { return send_card_and_app_list (ctrl, NULL, 0); } /* Send status lines with the serialno and appname of the current card * or of all cards if CARD is NULL. */ gpg_error_t app_send_active_apps (card_t card, ctrl_t ctrl) { return send_card_and_app_list (ctrl, card, 1); } /* Switch to APPNAME and print a respective status line with that app * listed first. If APPNAME is NULL or the empty string no switching * is done but the status line is printed anyway. */ gpg_error_t app_switch_active_app (card_t card, ctrl_t ctrl, const char *appname) { gpg_error_t err; apptype_t apptype; /* Note that in case the additional applications have not yet been * added to the card context (which is commonly done by means of * "SERIALNO --all", we do that here. */ err = select_all_additional_applications_internal (ctrl, card); if (err) goto leave; if (appname && *appname) { apptype = apptype_from_name (appname); if (!apptype) { err = gpg_error (GPG_ERR_NOT_FOUND); goto leave; } ctrl->current_apptype = apptype; err = maybe_switch_app (ctrl, card, NULL); if (err) goto leave; } /* Print the status line. */ err = send_serialno_and_app_status (card, 1, ctrl); leave: return err; } /* Execute an action for each app. ACTION can be one of: * * - KEYGRIP_ACTION_SEND_DATA * * If KEYGRIP_STR matches a public key of any active application * send information as LF terminated data lines about the public * key. The format of these lines is * T * If a match was found a pointer to the matching application is * returned. With the KEYGRIP_STR given as NULL, lines for all * keys (with CAPABILITY) will be send and the return value is * GPG_ERR_TRUE. * * - KEYGRIP_ACTION_WRITE_STATUS * * Same as KEYGRIP_ACTION_SEND_DATA but uses status lines instead * of data lines. * * - KEYGRIP_ACTION_LOOKUP * * Returns a pointer to the application matching KEYGRIP_STR but * does not emit any status or data lines. If no key with that * keygrip is available or KEYGRIP_STR is NULL, GPG_ERR_NOT_FOUND * is returned. */ card_t app_do_with_keygrip (ctrl_t ctrl, int action, const char *keygrip_str, int capability) { card_t card; card_list_r_lock (); card = do_with_keygrip (ctrl, action, keygrip_str, capability); card_list_r_unlock (); return card; } diff --git a/tests/gpgscm/ffi.c b/tests/gpgscm/ffi.c index ce18e0794..b46d5cb61 100644 --- a/tests/gpgscm/ffi.c +++ b/tests/gpgscm/ffi.c @@ -1,1478 +1,1486 @@ /* FFI interface for TinySCHEME. * * Copyright (C) 2016 g10 code GmbH * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_LIBREADLINE #define GNUPG_LIBREADLINE_H_INCLUDED #include #include #endif #include "../../common/util.h" +#ifdef HAVE_W32_SYSTEM +#define NEED_STRUCT_SPAWN_CB_ARG +#endif #include "../../common/exechelp.h" #include "../../common/sysutils.h" #ifdef HAVE_W32_SYSTEM #include #endif #include "private.h" #include "ffi.h" #include "ffi-private.h" /* For use in nice error messages. */ static const char * ordinal_suffix (int n) { switch (n) { case 1: return "st"; case 2: return "nd"; case 3: return "rd"; default: return "th"; } assert (! "reached"); } int ffi_bool_value (scheme *sc, pointer p) { return ! (p == sc->F); } static pointer do_logand (scheme *sc, pointer args) { FFI_PROLOG (); unsigned int v, acc = ~0; while (args != sc->NIL) { FFI_ARG_OR_RETURN (sc, unsigned int, v, number, args); acc &= v; } FFI_RETURN_INT (sc, acc); } static pointer do_logior (scheme *sc, pointer args) { FFI_PROLOG (); unsigned int v, acc = 0; while (args != sc->NIL) { FFI_ARG_OR_RETURN (sc, unsigned int, v, number, args); acc |= v; } FFI_RETURN_INT (sc, acc); } static pointer do_logxor (scheme *sc, pointer args) { FFI_PROLOG (); unsigned int v, acc = 0; while (args != sc->NIL) { FFI_ARG_OR_RETURN (sc, unsigned int, v, number, args); acc ^= v; } FFI_RETURN_INT (sc, acc); } static pointer do_lognot (scheme *sc, pointer args) { FFI_PROLOG (); unsigned int v; FFI_ARG_OR_RETURN (sc, unsigned int, v, number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); FFI_RETURN_INT (sc, ~v); } /* User interface. */ static pointer do_flush_stdio (scheme *sc, pointer args) { FFI_PROLOG (); FFI_ARGS_DONE_OR_RETURN (sc, args); fflush (stdout); fflush (stderr); FFI_RETURN (sc); } int use_libreadline; /* Read a string, and return a pointer to it. Returns NULL on EOF. */ char * rl_gets (const char *prompt) { static char *line = NULL; char *p; xfree (line); #if HAVE_LIBREADLINE { line = readline (prompt); if (line && *line) add_history (line); } #else { size_t max_size = 0xff; printf ("%s", prompt); fflush (stdout); line = xtrymalloc (max_size); if (line != NULL) fgets (line, max_size, stdin); } #endif /* Strip trailing whitespace. */ if (line && strlen (line) > 0) for (p = &line[strlen (line) - 1]; isspace (*p); p--) *p = 0; return line; } static pointer do_prompt (scheme *sc, pointer args) { FFI_PROLOG (); const char *prompt; const char *line; FFI_ARG_OR_RETURN (sc, const char *, prompt, string, args); FFI_ARGS_DONE_OR_RETURN (sc, args); line = rl_gets (prompt); if (! line) FFI_RETURN_POINTER (sc, sc->EOF_OBJ); FFI_RETURN_STRING (sc, line); } static pointer do_sleep (scheme *sc, pointer args) { FFI_PROLOG (); unsigned int seconds; FFI_ARG_OR_RETURN (sc, unsigned int, seconds, number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); sleep (seconds); FFI_RETURN (sc); } static pointer do_usleep (scheme *sc, pointer args) { FFI_PROLOG (); useconds_t microseconds; FFI_ARG_OR_RETURN (sc, useconds_t, microseconds, number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); usleep (microseconds); FFI_RETURN (sc); } static pointer do_chdir (scheme *sc, pointer args) { FFI_PROLOG (); char *name; FFI_ARG_OR_RETURN (sc, char *, name, path, args); FFI_ARGS_DONE_OR_RETURN (sc, args); if (chdir (name)) FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); FFI_RETURN (sc); } static pointer do_strerror (scheme *sc, pointer args) { FFI_PROLOG (); int error; FFI_ARG_OR_RETURN (sc, int, error, number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); FFI_RETURN_STRING (sc, gpg_strerror (error)); } static pointer do_getenv (scheme *sc, pointer args) { FFI_PROLOG (); char *name; char *value; FFI_ARG_OR_RETURN (sc, char *, name, string, args); FFI_ARGS_DONE_OR_RETURN (sc, args); value = getenv (name); FFI_RETURN_STRING (sc, value ? value : ""); } static pointer do_setenv (scheme *sc, pointer args) { FFI_PROLOG (); char *name; char *value; int overwrite; FFI_ARG_OR_RETURN (sc, char *, name, string, args); FFI_ARG_OR_RETURN (sc, char *, value, string, args); FFI_ARG_OR_RETURN (sc, int, overwrite, bool, args); FFI_ARGS_DONE_OR_RETURN (sc, args); if (gnupg_setenv (name, value, overwrite)) FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); FFI_RETURN (sc); } static pointer do_exit (scheme *sc, pointer args) { FFI_PROLOG (); int retcode; FFI_ARG_OR_RETURN (sc, int, retcode, number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); exit (retcode); } /* XXX: use gnupgs variant b/c mode as string */ static pointer do_open (scheme *sc, pointer args) { FFI_PROLOG (); int fd; char *pathname; int flags; mode_t mode = 0; FFI_ARG_OR_RETURN (sc, char *, pathname, path, args); FFI_ARG_OR_RETURN (sc, int, flags, number, args); if (args != sc->NIL) FFI_ARG_OR_RETURN (sc, mode_t, mode, number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); fd = open (pathname, flags, mode); if (fd == -1) FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); FFI_RETURN_INT (sc, fd); } static pointer do_fdopen (scheme *sc, pointer args) { FFI_PROLOG (); FILE *stream; int fd; char *mode; int kind; FFI_ARG_OR_RETURN (sc, int, fd, number, args); FFI_ARG_OR_RETURN (sc, char *, mode, string, args); FFI_ARGS_DONE_OR_RETURN (sc, args); stream = fdopen (fd, mode); if (stream == NULL) FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); if (setvbuf (stream, NULL, _IONBF, 0) != 0) FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); kind = 0; if (strchr (mode, 'r')) kind |= port_input; if (strchr (mode, 'w')) kind |= port_output; FFI_RETURN_POINTER (sc, sc->vptr->mk_port_from_file (sc, stream, kind)); } static pointer do_close (scheme *sc, pointer args) { FFI_PROLOG (); int fd; FFI_ARG_OR_RETURN (sc, int, fd, number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); FFI_RETURN_ERR (sc, close (fd) == 0 ? 0 : gpg_error_from_syserror ()); } static pointer do_seek (scheme *sc, pointer args) { FFI_PROLOG (); int fd; off_t offset; int whence; FFI_ARG_OR_RETURN (sc, int, fd, number, args); FFI_ARG_OR_RETURN (sc, off_t, offset, number, args); FFI_ARG_OR_RETURN (sc, int, whence, number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); FFI_RETURN_ERR (sc, lseek (fd, offset, whence) == (off_t) -1 ? gpg_error_from_syserror () : 0); } static pointer do_get_temp_path (scheme *sc, pointer args) { FFI_PROLOG (); #ifdef HAVE_W32_SYSTEM char buffer[MAX_PATH+1]; #endif FFI_ARGS_DONE_OR_RETURN (sc, args); #ifdef HAVE_W32_SYSTEM if (GetTempPath (MAX_PATH+1, buffer) == 0) FFI_RETURN_STRING (sc, "/temp"); else { size_t len = strlen (buffer); buffer[len-1] = 0; } FFI_RETURN_STRING (sc, buffer); #else FFI_RETURN_STRING (sc, "/tmp"); #endif } static pointer do_mkdtemp (scheme *sc, pointer args) { FFI_PROLOG (); char *template; #ifdef PATH_MAX char buffer[PATH_MAX]; #else char buffer[1024]; #endif char *name; FFI_ARG_OR_RETURN (sc, char *, template, string, args); FFI_ARGS_DONE_OR_RETURN (sc, args); if (strlen (template) > sizeof buffer - 1) FFI_RETURN_ERR (sc, EINVAL); strncpy (buffer, template, sizeof buffer); name = gnupg_mkdtemp (buffer); if (name == NULL) FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); FFI_RETURN_STRING (sc, name); } static pointer do_unlink (scheme *sc, pointer args) { FFI_PROLOG (); char *name; FFI_ARG_OR_RETURN (sc, char *, name, string, args); FFI_ARGS_DONE_OR_RETURN (sc, args); if (unlink (name) == -1) FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); FFI_RETURN (sc); } static gpg_error_t unlink_recursively (const char *name) { gpg_error_t err = 0; struct stat st; if (stat (name, &st) == -1) return gpg_error_from_syserror (); if (S_ISDIR (st.st_mode)) { DIR *dir; struct dirent *dent; dir = opendir (name); if (dir == NULL) return gpg_error_from_syserror (); while ((dent = readdir (dir))) { char *child; if (strcmp (dent->d_name, ".") == 0 || strcmp (dent->d_name, "..") == 0) continue; child = xtryasprintf ("%s/%s", name, dent->d_name); if (child == NULL) { err = gpg_error_from_syserror (); goto leave; } err = unlink_recursively (child); xfree (child); if (err == gpg_error_from_errno (ENOENT)) err = 0; if (err) goto leave; } leave: closedir (dir); if (! err) rmdir (name); return err; } else if (unlink (name) == -1) return gpg_error_from_syserror (); return 0; } static pointer do_unlink_recursively (scheme *sc, pointer args) { FFI_PROLOG (); char *name; FFI_ARG_OR_RETURN (sc, char *, name, string, args); FFI_ARGS_DONE_OR_RETURN (sc, args); err = unlink_recursively (name); FFI_RETURN (sc); } static pointer do_rename (scheme *sc, pointer args) { FFI_PROLOG (); char *old; char *new; FFI_ARG_OR_RETURN (sc, char *, old, string, args); FFI_ARG_OR_RETURN (sc, char *, new, string, args); FFI_ARGS_DONE_OR_RETURN (sc, args); if (rename (old, new) == -1) FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); FFI_RETURN (sc); } static pointer do_getcwd (scheme *sc, pointer args) { FFI_PROLOG (); pointer result; char *cwd; FFI_ARGS_DONE_OR_RETURN (sc, args); cwd = gnupg_getcwd (); if (cwd == NULL) FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); result = sc->vptr->mk_string (sc, cwd); xfree (cwd); FFI_RETURN_POINTER (sc, result); } static pointer do_mkdir (scheme *sc, pointer args) { FFI_PROLOG (); char *name; char *mode; FFI_ARG_OR_RETURN (sc, char *, name, string, args); FFI_ARG_OR_RETURN (sc, char *, mode, string, args); FFI_ARGS_DONE_OR_RETURN (sc, args); if (gnupg_mkdir (name, mode) == -1) FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); FFI_RETURN (sc); } static pointer do_rmdir (scheme *sc, pointer args) { FFI_PROLOG (); char *name; FFI_ARG_OR_RETURN (sc, char *, name, string, args); FFI_ARGS_DONE_OR_RETURN (sc, args); if (rmdir (name) == -1) FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); FFI_RETURN (sc); } static pointer do_get_isotime (scheme *sc, pointer args) { FFI_PROLOG (); gnupg_isotime_t timebuf; FFI_ARGS_DONE_OR_RETURN (sc, args); gnupg_get_isotime (timebuf); FFI_RETURN_STRING (sc, timebuf); } static pointer do_get_time (scheme *sc, pointer args) { FFI_PROLOG (); FFI_ARGS_DONE_OR_RETURN (sc, args); FFI_RETURN_INT (sc, gnupg_get_time ()); } static pointer do_getpid (scheme *sc, pointer args) { FFI_PROLOG (); FFI_ARGS_DONE_OR_RETURN (sc, args); FFI_RETURN_INT (sc, getpid ()); } static pointer do_srandom (scheme *sc, pointer args) { FFI_PROLOG (); int seed; FFI_ARG_OR_RETURN (sc, int, seed, number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); srand (seed); FFI_RETURN (sc); } static int random_scaled (int scale) { int v; #ifdef HAVE_RAND v = rand (); #else v = random (); #endif #ifndef RAND_MAX /* for SunOS */ #define RAND_MAX 32767 #endif return ((int) (1 + (int) ((float) scale * v / (RAND_MAX + 1.0))) - 1); } static pointer do_random (scheme *sc, pointer args) { FFI_PROLOG (); int scale; FFI_ARG_OR_RETURN (sc, int, scale, number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); FFI_RETURN_INT (sc, random_scaled (scale)); } static pointer do_make_random_string (scheme *sc, pointer args) { FFI_PROLOG (); int size; pointer chunk; char *p; FFI_ARG_OR_RETURN (sc, int, size, number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); if (size < 0) return ffi_sprintf (sc, "size must be positive"); chunk = sc->vptr->mk_counted_string (sc, NULL, size); if (sc->no_memory) FFI_RETURN_ERR (sc, ENOMEM); for (p = sc->vptr->string_value (chunk); size; p++, size--) *p = (char) random_scaled (256); FFI_RETURN_POINTER (sc, chunk); } /* estream functions. */ struct es_object_box { estream_t stream; int closed; }; static void es_object_finalize (scheme *sc, void *data) { struct es_object_box *box = data; (void) sc; if (! box->closed) es_fclose (box->stream); xfree (box); } static void es_object_to_string (scheme *sc, char *out, size_t size, void *data) { struct es_object_box *box = data; (void) sc; snprintf (out, size, "#estream %p", box->stream); } static struct foreign_object_vtable es_object_vtable = { es_object_finalize, es_object_to_string, }; static pointer es_wrap (scheme *sc, estream_t stream) { struct es_object_box *box = xmalloc (sizeof *box); if (box == NULL) return sc->NIL; box->stream = stream; box->closed = 0; return sc->vptr->mk_foreign_object (sc, &es_object_vtable, box); } static struct es_object_box * es_unwrap (scheme *sc, pointer object) { (void) sc; if (! is_foreign_object (object)) return NULL; if (sc->vptr->get_foreign_object_vtable (object) != &es_object_vtable) return NULL; return sc->vptr->get_foreign_object_data (object); } #define CONVERSION_estream(SC, X) es_unwrap (SC, X) #define IS_A_estream(SC, X) es_unwrap (SC, X) static pointer do_es_fclose (scheme *sc, pointer args) { FFI_PROLOG (); struct es_object_box *box; FFI_ARG_OR_RETURN (sc, struct es_object_box *, box, estream, args); FFI_ARGS_DONE_OR_RETURN (sc, args); err = es_fclose (box->stream); if (! err) box->closed = 1; FFI_RETURN (sc); } static pointer do_es_read (scheme *sc, pointer args) { FFI_PROLOG (); struct es_object_box *box; size_t bytes_to_read; pointer result; void *buffer; size_t bytes_read; FFI_ARG_OR_RETURN (sc, struct es_object_box *, box, estream, args); FFI_ARG_OR_RETURN (sc, size_t, bytes_to_read, number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); buffer = xtrymalloc (bytes_to_read); if (buffer == NULL) FFI_RETURN_ERR (sc, ENOMEM); err = es_read (box->stream, buffer, bytes_to_read, &bytes_read); if (err) FFI_RETURN_ERR (sc, err); result = sc->vptr->mk_counted_string (sc, buffer, bytes_read); xfree (buffer); FFI_RETURN_POINTER (sc, result); } static pointer do_es_feof (scheme *sc, pointer args) { FFI_PROLOG (); struct es_object_box *box; FFI_ARG_OR_RETURN (sc, struct es_object_box *, box, estream, args); FFI_ARGS_DONE_OR_RETURN (sc, args); FFI_RETURN_POINTER (sc, es_feof (box->stream) ? sc->T : sc->F); } static pointer do_es_write (scheme *sc, pointer args) { FFI_PROLOG (); struct es_object_box *box; const char *buffer; size_t bytes_to_write, bytes_written; FFI_ARG_OR_RETURN (sc, struct es_object_box *, box, estream, args); /* XXX how to get the length of the string buffer? scheme strings may contain \0. */ FFI_ARG_OR_RETURN (sc, const char *, buffer, string, args); FFI_ARGS_DONE_OR_RETURN (sc, args); bytes_to_write = strlen (buffer); while (bytes_to_write > 0) { err = es_write (box->stream, buffer, bytes_to_write, &bytes_written); if (err) break; bytes_to_write -= bytes_written; buffer += bytes_written; } FFI_RETURN (sc); } - /* Process handling. */ +struct proc_object_box +{ + gnupg_process_t proc; +}; + +static void +proc_object_finalize (scheme *sc, void *data) +{ + struct proc_object_box *box = data; + (void) sc; + + if (!box->proc) + gnupg_process_release (box->proc); + xfree (box); +} + +static void +proc_object_to_string (scheme *sc, char *out, size_t size, void *data) +{ + struct proc_object_box *box = data; + (void) sc; + + snprintf (out, size, "#proc %p", box->proc); +} + +static struct foreign_object_vtable proc_object_vtable = + { + proc_object_finalize, + proc_object_to_string, + }; + static pointer -do_spawn_process (scheme *sc, pointer args) +proc_wrap (scheme *sc, gnupg_process_t proc) +{ + struct proc_object_box *box = xmalloc (sizeof *box); + if (box == NULL) + return sc->NIL; + + box->proc = proc; + return sc->vptr->mk_foreign_object (sc, &proc_object_vtable, box); +} + +static struct proc_object_box * +proc_unwrap (scheme *sc, pointer object) +{ + (void) sc; + + if (! is_foreign_object (object)) + return NULL; + + if (sc->vptr->get_foreign_object_vtable (object) != &proc_object_vtable) + return NULL; + + return sc->vptr->get_foreign_object_data (object); +} + +#define CONVERSION_proc(SC, X) proc_unwrap (SC, X) +#define IS_A_proc(SC, X) proc_unwrap (SC, X) + + +static pointer +do_process_spawn (scheme *sc, pointer args) { FFI_PROLOG (); pointer arguments; char **argv; size_t len; unsigned int flags; - + gnupg_process_t proc = NULL; estream_t infp; estream_t outfp; estream_t errfp; - pid_t pid; FFI_ARG_OR_RETURN (sc, pointer, arguments, list, args); FFI_ARG_OR_RETURN (sc, unsigned int, flags, number, args); + flags |= (GNUPG_PROCESS_STDIN_PIPE + | GNUPG_PROCESS_STDOUT_PIPE + | GNUPG_PROCESS_STDERR_PIPE); FFI_ARGS_DONE_OR_RETURN (sc, args); err = ffi_list2argv (sc, arguments, &argv, &len); if (err == gpg_error (GPG_ERR_INV_VALUE)) return ffi_sprintf (sc, "%luth element of first argument is " "neither string nor symbol", (unsigned long) len); if (err) FFI_RETURN_ERR (sc, err); if (verbose > 1) { char **p; fprintf (stderr, "Executing:"); for (p = argv; *p; p++) fprintf (stderr, " '%s'", *p); fprintf (stderr, "\n"); } - err = gnupg_spawn_process (argv[0], (const char **) &argv[1], - NULL, - flags, - &infp, &outfp, &errfp, &pid); + err = gnupg_process_spawn (argv[0], (const char **) &argv[1], + flags, NULL, NULL, &proc); + err = gnupg_process_get_streams (proc, 0, &infp, &outfp, &errfp); xfree (argv); -#define IMC(A, B) \ - _cons (sc, sc->vptr->mk_integer (sc, (unsigned long) (A)), (B), 1) +#define IMP(A, B) \ + _cons (sc, proc_wrap (sc, (A)), (B), 1) #define IMS(A, B) \ _cons (sc, es_wrap (sc, (A)), (B), 1) FFI_RETURN_POINTER (sc, IMS (infp, IMS (outfp, IMS (errfp, - IMC (pid, sc->NIL))))); + IMP (proc, sc->NIL))))); #undef IMS #undef IMC } +static void +setup_std_fds (struct spawn_cb_arg *sca) +{ + int *std_fds = sca->arg; + +#ifdef HAVE_W32_SYSTEM + sca->hd[0] = std_fds[0] == -1? + INVALID_HANDLE_VALUE : (HANDLE)_get_osfhandle (std_fds[0]); + sca->hd[1] = std_fds[1] == -1? + INVALID_HANDLE_VALUE : (HANDLE)_get_osfhandle (std_fds[1]); + sca->hd[2] = std_fds[2] == -1? + INVALID_HANDLE_VALUE : (HANDLE)_get_osfhandle (std_fds[2]); +#else + sca->fds[0] = std_fds[0]; + sca->fds[1] = std_fds[1]; + sca->fds[2] = std_fds[2]; +#endif +} + static pointer -do_spawn_process_fd (scheme *sc, pointer args) +do_process_spawn_fd (scheme *sc, pointer args) { FFI_PROLOG (); pointer arguments; char **argv; size_t len; - int infd, outfd, errfd; - - pid_t pid; + int std_fds[3]; + gnupg_process_t proc = NULL; FFI_ARG_OR_RETURN (sc, pointer, arguments, list, args); - FFI_ARG_OR_RETURN (sc, int, infd, number, args); - FFI_ARG_OR_RETURN (sc, int, outfd, number, args); - FFI_ARG_OR_RETURN (sc, int, errfd, number, args); + FFI_ARG_OR_RETURN (sc, int, std_fds[0], number, args); + FFI_ARG_OR_RETURN (sc, int, std_fds[1], number, args); + FFI_ARG_OR_RETURN (sc, int, std_fds[2], number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); err = ffi_list2argv (sc, arguments, &argv, &len); if (err == gpg_error (GPG_ERR_INV_VALUE)) return ffi_sprintf (sc, "%luth element of first argument is " "neither string nor symbol", (unsigned long) len); if (err) FFI_RETURN_ERR (sc, err); if (verbose > 1) { char **p; fprintf (stderr, "Executing:"); for (p = argv; *p; p++) fprintf (stderr, " '%s'", *p); - fprintf (stderr, "\n"); + fprintf (stderr, " (%d %d %d)\n", std_fds[0], std_fds[1], std_fds[2]); } - err = gnupg_spawn_process_fd (argv[0], (const char **) &argv[1], - infd, outfd, errfd, &pid); + err = gnupg_process_spawn (argv[0], (const char **) &argv[1], + 0, setup_std_fds, std_fds, &proc); xfree (argv); - FFI_RETURN_INT (sc, pid); + FFI_RETURN_POINTER (sc, proc_wrap (sc, proc)); } static pointer -do_wait_process (scheme *sc, pointer args) +do_process_wait (scheme *sc, pointer args) { FFI_PROLOG (); - const char *name; - pid_t pid; + struct proc_object_box *box; int hang; + int retcode = -1; - int retcode; - - FFI_ARG_OR_RETURN (sc, const char *, name, string, args); - FFI_ARG_OR_RETURN (sc, pid_t, pid, number, args); + FFI_ARG_OR_RETURN (sc, struct proc_object_box *, box, proc, args); FFI_ARG_OR_RETURN (sc, int, hang, bool, args); FFI_ARGS_DONE_OR_RETURN (sc, args); - err = gnupg_wait_process (name, pid, hang, &retcode); - if (err == GPG_ERR_GENERAL) - err = 0; /* Let the return code speak for itself. */ - - FFI_RETURN_INT (sc, retcode); -} - - -static pointer -do_wait_processes (scheme *sc, pointer args) -{ - FFI_PROLOG (); - pointer list_names; - char **names; - pointer list_pids; - size_t i, count; - pid_t *pids; - int hang; - int *retcodes; - pointer retcodes_list = sc->NIL; - - FFI_ARG_OR_RETURN (sc, pointer, list_names, list, args); - FFI_ARG_OR_RETURN (sc, pointer, list_pids, list, args); - FFI_ARG_OR_RETURN (sc, int, hang, bool, args); - FFI_ARGS_DONE_OR_RETURN (sc, args); - - if (sc->vptr->list_length (sc, list_names) - != sc->vptr->list_length (sc, list_pids)) - return - sc->vptr->mk_string (sc, "length of first two arguments must match"); - - err = ffi_list2argv (sc, list_names, &names, &count); - if (err == gpg_error (GPG_ERR_INV_VALUE)) - return ffi_sprintf (sc, "%lu%s element of first argument is " - "neither string nor symbol", - (unsigned long) count, - ordinal_suffix ((int) count)); - if (err) - FFI_RETURN_ERR (sc, err); - - err = ffi_list2intv (sc, list_pids, (int **) &pids, &count); - if (err == gpg_error (GPG_ERR_INV_VALUE)) - return ffi_sprintf (sc, "%lu%s element of second argument is " - "not a number", - (unsigned long) count, - ordinal_suffix ((int) count)); - if (err) - FFI_RETURN_ERR (sc, err); - - retcodes = xtrycalloc (sizeof *retcodes, count); - if (retcodes == NULL) - { - xfree (names); - xfree (pids); - FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); - } - - err = gnupg_wait_processes ((const char **) names, pids, count, hang, - retcodes); - if (err == GPG_ERR_GENERAL) - err = 0; /* Let the return codes speak. */ + err = gnupg_process_wait (box->proc, hang); + if (!err) + err = gnupg_process_ctl (box->proc, GNUPG_PROCESS_GET_EXIT_ID, &retcode); if (err == GPG_ERR_TIMEOUT) - err = 0; /* We may have got some results. */ - - for (i = 0; i < count; i++) - retcodes_list = - (sc->vptr->cons) (sc, - sc->vptr->mk_integer (sc, - (long) retcodes[count-1-i]), - retcodes_list); + err = 0; - xfree (names); - xfree (pids); - xfree (retcodes); - FFI_RETURN_POINTER (sc, retcodes_list); + FFI_RETURN_INT (sc, retcode); } - static pointer do_pipe (scheme *sc, pointer args) { FFI_PROLOG (); int filedes[2]; FFI_ARGS_DONE_OR_RETURN (sc, args); err = gnupg_create_pipe (filedes); #define IMC(A, B) \ _cons (sc, sc->vptr->mk_integer (sc, (unsigned long) (A)), (B), 1) FFI_RETURN_POINTER (sc, IMC (filedes[0], IMC (filedes[1], sc->NIL))); #undef IMC } static pointer do_inbound_pipe (scheme *sc, pointer args) { FFI_PROLOG (); int filedes[2]; FFI_ARGS_DONE_OR_RETURN (sc, args); err = gnupg_create_inbound_pipe (filedes, NULL, 0); #define IMC(A, B) \ _cons (sc, sc->vptr->mk_integer (sc, (unsigned long) (A)), (B), 1) FFI_RETURN_POINTER (sc, IMC (filedes[0], IMC (filedes[1], sc->NIL))); #undef IMC } static pointer do_outbound_pipe (scheme *sc, pointer args) { FFI_PROLOG (); int filedes[2]; FFI_ARGS_DONE_OR_RETURN (sc, args); err = gnupg_create_outbound_pipe (filedes, NULL, 0); #define IMC(A, B) \ _cons (sc, sc->vptr->mk_integer (sc, (unsigned long) (A)), (B), 1) FFI_RETURN_POINTER (sc, IMC (filedes[0], IMC (filedes[1], sc->NIL))); #undef IMC } /* Test helper functions. */ static pointer do_file_equal (scheme *sc, pointer args) { FFI_PROLOG (); pointer result = sc->F; char *a_name, *b_name; int binary; const char *mode; FILE *a_stream = NULL, *b_stream = NULL; struct stat a_stat, b_stat; #define BUFFER_SIZE 1024 char a_buf[BUFFER_SIZE], b_buf[BUFFER_SIZE]; #undef BUFFER_SIZE size_t chunk; FFI_ARG_OR_RETURN (sc, char *, a_name, string, args); FFI_ARG_OR_RETURN (sc, char *, b_name, string, args); FFI_ARG_OR_RETURN (sc, int, binary, bool, args); FFI_ARGS_DONE_OR_RETURN (sc, args); mode = binary ? "rb" : "r"; a_stream = fopen (a_name, mode); if (a_stream == NULL) goto errout; b_stream = fopen (b_name, mode); if (b_stream == NULL) goto errout; if (fstat (fileno (a_stream), &a_stat) < 0) goto errout; if (fstat (fileno (b_stream), &b_stat) < 0) goto errout; if (binary && a_stat.st_size != b_stat.st_size) { if (verbose) fprintf (stderr, "Files %s and %s differ in size %lu != %lu\n", a_name, b_name, (unsigned long) a_stat.st_size, (unsigned long) b_stat.st_size); goto out; } while (! feof (a_stream)) { chunk = sizeof a_buf; chunk = fread (a_buf, 1, chunk, a_stream); if (chunk == 0 && ferror (a_stream)) goto errout; /* some error */ if (fread (b_buf, 1, chunk, b_stream) < chunk) { if (feof (b_stream)) goto out; /* short read */ goto errout; /* some error */ } if (chunk > 0 && memcmp (a_buf, b_buf, chunk) != 0) goto out; } fread (b_buf, 1, 1, b_stream); if (! feof (b_stream)) goto out; /* b is longer */ /* They match. */ result = sc->T; out: if (a_stream) fclose (a_stream); if (b_stream) fclose (b_stream); FFI_RETURN_POINTER (sc, result); errout: err = gpg_error_from_syserror (); goto out; } static pointer do_splice (scheme *sc, pointer args) { FFI_PROLOG (); int source; char buffer[1024]; ssize_t bytes_read; pointer sinks, sink; FFI_ARG_OR_RETURN (sc, int, source, number, args); sinks = args; if (sinks == sc->NIL) return ffi_sprintf (sc, "need at least one sink"); for (sink = sinks; sink != sc->NIL; sink = pair_cdr (sink), ffi_arg_index++) if (! sc->vptr->is_number (pair_car (sink))) return ffi_sprintf (sc, "%d%s argument is not a number", ffi_arg_index, ordinal_suffix (ffi_arg_index)); while (1) { bytes_read = read (source, buffer, sizeof buffer); if (bytes_read == 0) break; if (bytes_read < 0) FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); for (sink = sinks; sink != sc->NIL; sink = pair_cdr (sink)) { int fd = sc->vptr->ivalue (pair_car (sink)); char *p = buffer; ssize_t left = bytes_read; while (left) { ssize_t written = write (fd, p, left); if (written < 0) FFI_RETURN_ERR (sc, gpg_error_from_syserror ()); assert (written <= left); left -= written; p += written; } } } FFI_RETURN (sc); } static pointer do_string_index (scheme *sc, pointer args) { FFI_PROLOG (); char *haystack; char needle; ssize_t offset = 0; char *position; FFI_ARG_OR_RETURN (sc, char *, haystack, string, args); FFI_ARG_OR_RETURN (sc, char, needle, character, args); if (args != sc->NIL) { FFI_ARG_OR_RETURN (sc, ssize_t, offset, number, args); if (offset < 0) return ffi_sprintf (sc, "offset must be positive"); if (offset > strlen (haystack)) return ffi_sprintf (sc, "offset exceeds haystack"); } FFI_ARGS_DONE_OR_RETURN (sc, args); position = strchr (haystack+offset, needle); if (position) FFI_RETURN_INT (sc, position - haystack); else FFI_RETURN_POINTER (sc, sc->F); } static pointer do_string_rindex (scheme *sc, pointer args) { FFI_PROLOG (); char *haystack; char needle; ssize_t offset = 0; char *position; FFI_ARG_OR_RETURN (sc, char *, haystack, string, args); FFI_ARG_OR_RETURN (sc, char, needle, character, args); if (args != sc->NIL) { FFI_ARG_OR_RETURN (sc, ssize_t, offset, number, args); if (offset < 0) return ffi_sprintf (sc, "offset must be positive"); if (offset > strlen (haystack)) return ffi_sprintf (sc, "offset exceeds haystack"); } FFI_ARGS_DONE_OR_RETURN (sc, args); position = strrchr (haystack+offset, needle); if (position) FFI_RETURN_INT (sc, position - haystack); else FFI_RETURN_POINTER (sc, sc->F); } static pointer do_string_contains (scheme *sc, pointer args) { FFI_PROLOG (); char *haystack; char *needle; FFI_ARG_OR_RETURN (sc, char *, haystack, string, args); FFI_ARG_OR_RETURN (sc, char *, needle, string, args); FFI_ARGS_DONE_OR_RETURN (sc, args); FFI_RETURN_POINTER (sc, strstr (haystack, needle) ? sc->T : sc->F); } static pointer do_get_verbose (scheme *sc, pointer args) { FFI_PROLOG (); FFI_ARGS_DONE_OR_RETURN (sc, args); FFI_RETURN_INT (sc, verbose); } static pointer do_set_verbose (scheme *sc, pointer args) { FFI_PROLOG (); int new_verbosity, old; FFI_ARG_OR_RETURN (sc, int, new_verbosity, number, args); FFI_ARGS_DONE_OR_RETURN (sc, args); old = verbose; verbose = new_verbosity; FFI_RETURN_INT (sc, old); } gpg_error_t ffi_list2argv (scheme *sc, pointer list, char ***argv, size_t *len) { int i; *len = sc->vptr->list_length (sc, list); *argv = xtrycalloc (*len + 1, sizeof **argv); if (*argv == NULL) return gpg_error_from_syserror (); for (i = 0; sc->vptr->is_pair (list); list = sc->vptr->pair_cdr (list)) { if (sc->vptr->is_string (sc->vptr->pair_car (list))) (*argv)[i++] = sc->vptr->string_value (sc->vptr->pair_car (list)); else if (sc->vptr->is_symbol (sc->vptr->pair_car (list))) (*argv)[i++] = sc->vptr->symname (sc->vptr->pair_car (list)); else { xfree (*argv); *argv = NULL; *len = i; return gpg_error (GPG_ERR_INV_VALUE); } } (*argv)[i] = NULL; return 0; } gpg_error_t ffi_list2intv (scheme *sc, pointer list, int **intv, size_t *len) { int i; *len = sc->vptr->list_length (sc, list); *intv = xtrycalloc (*len, sizeof **intv); if (*intv == NULL) return gpg_error_from_syserror (); for (i = 0; sc->vptr->is_pair (list); list = sc->vptr->pair_cdr (list)) { if (sc->vptr->is_number (sc->vptr->pair_car (list))) (*intv)[i++] = sc->vptr->ivalue (sc->vptr->pair_car (list)); else { xfree (*intv); *intv = NULL; *len = i; return gpg_error (GPG_ERR_INV_VALUE); } } return 0; } char * ffi_schemify_name (const char *s, int macro) { /* Fixme: We should use xtrystrdup and return NULL. However, this * requires a lot more changes. Simply returning S as done * originally is not an option. */ char *n = xstrdup (s), *p; /* if (n == NULL) */ /* return s; */ for (p = n; *p; p++) { *p = (char) tolower (*p); /* We convert _ to - in identifiers. We allow, however, for function names to start with a leading _. The functions in this namespace are not yet finalized and might change or vanish without warning. Use them with care. */ if (! macro && p != n && *p == '_') *p = '-'; } return n; } pointer ffi_sprintf (scheme *sc, const char *format, ...) { pointer result; va_list listp; char *expression; int size, written; va_start (listp, format); size = vsnprintf (NULL, 0, format, listp); va_end (listp); expression = xtrymalloc (size + 1); if (expression == NULL) return NULL; va_start (listp, format); written = vsnprintf (expression, size + 1, format, listp); va_end (listp); assert (size == written); result = sc->vptr->mk_string (sc, expression); xfree (expression); return result; } void ffi_scheme_eval (scheme *sc, const char *format, ...) { va_list listp; char *expression; int size, written; va_start (listp, format); size = vsnprintf (NULL, 0, format, listp); va_end (listp); expression = xtrymalloc (size + 1); if (expression == NULL) return; va_start (listp, format); written = vsnprintf (expression, size + 1, format, listp); va_end (listp); assert (size == written); sc->vptr->load_string (sc, expression); xfree (expression); } gpg_error_t ffi_init (scheme *sc, const char *argv0, const char *scriptname, int argc, const char **argv) { int i; pointer args = sc->NIL; /* bitwise arithmetic */ ffi_define_function (sc, logand); ffi_define_function (sc, logior); ffi_define_function (sc, logxor); ffi_define_function (sc, lognot); /* libc. */ ffi_define_constant (sc, O_RDONLY); ffi_define_constant (sc, O_WRONLY); ffi_define_constant (sc, O_RDWR); ffi_define_constant (sc, O_CREAT); ffi_define_constant (sc, O_APPEND); #ifndef O_BINARY # define O_BINARY 0 #endif #ifndef O_TEXT # define O_TEXT 0 #endif ffi_define_constant (sc, O_BINARY); ffi_define_constant (sc, O_TEXT); ffi_define_constant (sc, STDIN_FILENO); ffi_define_constant (sc, STDOUT_FILENO); ffi_define_constant (sc, STDERR_FILENO); ffi_define_constant (sc, SEEK_SET); ffi_define_constant (sc, SEEK_CUR); ffi_define_constant (sc, SEEK_END); ffi_define_function (sc, sleep); ffi_define_function (sc, usleep); ffi_define_function (sc, chdir); ffi_define_function (sc, strerror); ffi_define_function (sc, getenv); ffi_define_function (sc, setenv); ffi_define_function_name (sc, "_exit", exit); ffi_define_function (sc, open); ffi_define_function (sc, fdopen); ffi_define_function (sc, close); ffi_define_function (sc, seek); ffi_define_function (sc, get_temp_path); ffi_define_function_name (sc, "_mkdtemp", mkdtemp); ffi_define_function (sc, unlink); ffi_define_function (sc, unlink_recursively); ffi_define_function (sc, rename); ffi_define_function (sc, getcwd); ffi_define_function (sc, mkdir); ffi_define_function (sc, rmdir); ffi_define_function (sc, get_isotime); ffi_define_function (sc, get_time); ffi_define_function (sc, getpid); /* Random numbers. */ ffi_define_function (sc, srandom); ffi_define_function (sc, random); ffi_define_function (sc, make_random_string); /* Process management. */ - ffi_define_function (sc, spawn_process); - ffi_define_function (sc, spawn_process_fd); - ffi_define_function (sc, wait_process); - ffi_define_function (sc, wait_processes); ffi_define_function (sc, pipe); ffi_define_function (sc, inbound_pipe); ffi_define_function (sc, outbound_pipe); + ffi_define_function (sc, process_spawn); + ffi_define_function (sc, process_spawn_fd); + ffi_define_function (sc, process_wait); /* estream functions. */ ffi_define_function_name (sc, "es-fclose", es_fclose); ffi_define_function_name (sc, "es-read", es_read); ffi_define_function_name (sc, "es-feof", es_feof); ffi_define_function_name (sc, "es-write", es_write); /* Test helper functions. */ ffi_define_function (sc, file_equal); ffi_define_function (sc, splice); ffi_define_function (sc, string_index); ffi_define_function (sc, string_rindex); ffi_define_function_name (sc, "string-contains?", string_contains); /* User interface. */ ffi_define_function (sc, flush_stdio); ffi_define_function (sc, prompt); /* Configuration. */ ffi_define_function_name (sc, "*verbose*", get_verbose); ffi_define_function_name (sc, "*set-verbose!*", set_verbose); ffi_define (sc, "*argv0*", sc->vptr->mk_string (sc, argv0)); ffi_define (sc, "*scriptname*", sc->vptr->mk_string (sc, scriptname)); for (i = argc - 1; i >= 0; i--) { pointer value = sc->vptr->mk_string (sc, argv[i]); args = (sc->vptr->cons) (sc, value, args); } ffi_define (sc, "*args*", args); #if _WIN32 ffi_define (sc, "*pathsep*", sc->vptr->mk_character (sc, ';')); #else ffi_define (sc, "*pathsep*", sc->vptr->mk_character (sc, ':')); #endif ffi_define (sc, "*win32*", #if _WIN32 sc->T #else sc->F #endif ); ffi_define (sc, "*maintainer-mode*", #if MAINTAINER_MODE sc->T #else sc->F #endif ); ffi_define (sc, "*run-all-tests*", #if RUN_ALL_TESTS sc->T #else sc->F #endif ); ffi_define (sc, "*stdin*", sc->vptr->mk_port_from_file (sc, stdin, port_input)); ffi_define (sc, "*stdout*", sc->vptr->mk_port_from_file (sc, stdout, port_output)); ffi_define (sc, "*stderr*", sc->vptr->mk_port_from_file (sc, stderr, port_output)); return 0; } diff --git a/tests/gpgscm/t-child.scm b/tests/gpgscm/t-child.scm index fd1dcc3fe..461413b9c 100644 --- a/tests/gpgscm/t-child.scm +++ b/tests/gpgscm/t-child.scm @@ -1,118 +1,117 @@ ;; Tests for the low-level process and IPC primitives. ;; ;; Copyright (C) 2016 g10 Code GmbH ;; ;; This file is part of GnuPG. ;; ;; GnuPG is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 3 of the License, or ;; (at your option) any later version. ;; ;; GnuPG is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program; if not, see . (echo "Testing process and IPC primitives...") (define (qualify executable) (string-append executable (getenv "EXEEXT"))) (define child (qualify "t-child")) (assert (= 0 (call `(,(qualify "t-child") "return0")))) (assert (= 1 (call `(,(qualify "t-child") "return1")))) (assert (= 77 (call `(,(qualify "t-child") "return77")))) (let ((r (call-with-io `(,(qualify "t-child") "return0") ""))) (assert (= 0 (:retcode r))) (assert (string=? "" (:stdout r))) (assert (string=? "" (:stderr r)))) (let ((r (call-with-io `(,(qualify "t-child") "return1") ""))) (assert (= 1 (:retcode r))) (assert (string=? "" (:stdout r))) (assert (string=? "" (:stderr r)))) (let ((r (call-with-io `(,(qualify "t-child") "return77") ""))) (assert (= 77 (:retcode r))) (assert (string=? "" (:stdout r))) (assert (string=? "" (:stderr r)))) (let ((r (call-with-io `(,(qualify "t-child") "hello_stdout") ""))) (assert (= 0 (:retcode r))) (assert (string=? "hello" (:stdout r))) (assert (string=? "" (:stderr r)))) (let ((r (call-with-io `(,(qualify "t-child") "hello_stderr") ""))) (assert (= 0 (:retcode r))) (assert (string=? "" (:stdout r))) (assert (string=? "hello" (:stderr r)))) (let ((r (call-with-io `(,(qualify "t-child") "stdout4096") ""))) (assert (= 0 (:retcode r))) (assert (= 4096 (string-length (:stdout r)))) (assert (string=? "" (:stderr r)))) (let ((r (call-with-io `(,(qualify "t-child") "stdout8192") ""))) (assert (= 0 (:retcode r))) (assert (= 8192 (string-length (:stdout r)))) (assert (string=? "" (:stderr r)))) (let ((r (call-with-io `(,(qualify "t-child") "cat") "hellohello"))) (assert (= 0 (:retcode r))) (assert (string=? "hellohello" (:stdout r))) (assert (string=? "" (:stderr r)))) (define (spawn what) - (spawn-process-fd what CLOSED_FD STDOUT_FILENO STDERR_FILENO)) + (process-spawn-fd what CLOSED_FD STDOUT_FILENO STDERR_FILENO)) -(let ((pid0 (spawn `(,(qualify "t-child") "return0"))) - (pid1 (spawn `(,(qualify "t-child") "return0")))) - (assert (equal? '(0 0) - (wait-processes '("child0" "child1") (list pid0 pid1) #t)))) +(let ((proc0 (spawn `(,(qualify "t-child") "return0"))) + (proc1 (spawn `(,(qualify "t-child") "return0")))) + (assert (= (process-wait proc0 #t) 0)) + (assert (= (process-wait proc1 #t) 0))) -(let ((pid0 (spawn `(,(qualify "t-child") "return1"))) - (pid1 (spawn `(,(qualify "t-child") "return0")))) - (assert (equal? '(1 0) - (wait-processes '("child0" "child1") (list pid0 pid1) #t)))) +(let ((proc0 (spawn `(,(qualify "t-child") "return1"))) + (proc1 (spawn `(,(qualify "t-child") "return0")))) + (assert (= (process-wait proc0 #t) 1)) + (assert (= (process-wait proc1 #t) 0))) -(let ((pid0 (spawn `(,(qualify "t-child") "return0"))) - (pid1 (spawn `(,(qualify "t-child") "return77"))) - (pid2 (spawn `(,(qualify "t-child") "return1")))) - (assert (equal? '(0 77 1) - (wait-processes '("child0" "child1" "child2") - (list pid0 pid1 pid2) #t)))) +(let ((proc0 (spawn `(,(qualify "t-child") "return0"))) + (proc1 (spawn `(,(qualify "t-child") "return77"))) + (proc2 (spawn `(,(qualify "t-child") "return1")))) + (assert (= (process-wait proc0 #t) 0)) + (assert (= (process-wait proc1 #t) 77)) + (assert (= (process-wait proc2 #t) 1))) (let* ((p (pipe)) - (pid0 (spawn-process-fd + (proc0 (process-spawn-fd `(,(qualify "t-child") "hello_stdout") CLOSED_FD (:write-end p) STDERR_FILENO)) (_ (close (:write-end p))) - (pid1 (spawn-process-fd + (proc1 (process-spawn-fd `(,(qualify "t-child") "cat") (:read-end p) STDOUT_FILENO STDERR_FILENO))) (close (:read-end p)) - (assert - (equal? '(0 0) - (wait-processes '("child0" "child1") (list pid0 pid1) #t)))) + (assert (= (process-wait proc0 #t) 0)) + (assert (= (process-wait proc1 #t) 0))) (echo " world.") (tr:do (tr:pipe-do (pipe:spawn `(,child stdout4096)) (pipe:spawn `(,child cat))) (tr:call-with-content (lambda (c) (assert (= 4096 (string-length c)))))) (tr:do (tr:pipe-do (pipe:spawn `(,child stdout8192)) (pipe:spawn `(,child cat))) (tr:call-with-content (lambda (c) (assert (= 8192 (string-length c)))))) (echo "All good.") diff --git a/tests/gpgscm/tests.scm b/tests/gpgscm/tests.scm index db1025bbb..6a11e55f1 100644 --- a/tests/gpgscm/tests.scm +++ b/tests/gpgscm/tests.scm @@ -1,902 +1,900 @@ ;; Common definitions for writing tests. ;; ;; Copyright (C) 2016 g10 Code GmbH ;; ;; This file is part of GnuPG. ;; ;; GnuPG is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 3 of the License, or ;; (at your option) any later version. ;; ;; GnuPG is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program; if not, see . ;; Reporting. (define (echo . msg) (for-each (lambda (x) (display x) (display " ")) msg) (newline)) (define (info . msg) (apply echo msg) (flush-stdio)) (define (log . msg) (if (> (*verbose*) 0) (apply info msg))) (define (fail . msg) (apply info msg) (exit 1)) (define (skip . msg) (apply info msg) (exit 77)) (define (make-counter) (let ((c 0)) (lambda () (let ((r c)) (set! c (+ 1 c)) r)))) (define *progress-nesting* 0) (define (call-with-progress msg what) (set! *progress-nesting* (+ 1 *progress-nesting*)) (if (= 1 *progress-nesting*) (begin (info msg) (display " > ") (flush-stdio) (what (lambda (item) (display item) (display " ") (flush-stdio))) (info "< ")) (begin (what (lambda (item) (display ".") (flush-stdio))) (display " ") (flush-stdio))) (set! *progress-nesting* (- *progress-nesting* 1))) (define (for-each-p msg proc lst . lsts) (apply for-each-p' `(,msg ,proc ,(lambda (x . xs) x) ,lst ,@lsts))) (define (for-each-p' msg proc fmt lst . lsts) (call-with-progress msg (lambda (progress) (apply for-each `(,(lambda args (progress (apply fmt args)) (apply proc args)) ,lst ,@lsts))))) ;; Process management. (define CLOSED_FD -1) (define (call-with-fds what infd outfd errfd) - (wait-process (stringify what) (spawn-process-fd what infd outfd errfd) #t)) + (process-wait (process-spawn-fd what infd outfd errfd) #t)) (define (call what) (call-with-fds what CLOSED_FD (if (< (*verbose*) 0) STDOUT_FILENO CLOSED_FD) (if (< (*verbose*) 0) STDERR_FILENO CLOSED_FD))) ;; Accessor functions for the results of 'spawn-process'. (define :stdin car) (define :stdout cadr) (define :stderr caddr) -(define :pid cadddr) +(define :proc cadddr) (define (call-with-io what in) - (let ((h (spawn-process what 0))) + (let ((h (process-spawn what 0))) (es-write (:stdin h) in) (es-fclose (:stdin h)) (let* ((out (es-read-all (:stdout h))) (err (es-read-all (:stderr h))) - (result (wait-process (car what) (:pid h) #t))) + (result (process-wait (:proc h) #t))) (es-fclose (:stdout h)) (es-fclose (:stderr h)) (if (> (*verbose*) 2) - (info "Child" (:pid h) "returned:" + (info "Child" (:proc h) "returned:" `((command ,(stringify what)) (status ,result) (stdout ,out) (stderr ,err)))) (list result out err)))) ;; Accessor function for the results of 'call-with-io'. ':stdout' and ;; ':stderr' can also be used. (define :retcode car) (define (call-check what) (let ((result (call-with-io what ""))) (if (= 0 (:retcode result)) (:stdout result) (throw (string-append (stringify what) " failed") (:stderr result))))) (define (call-popen command input-string) (let ((result (call-with-io command input-string))) (if (= 0 (:retcode result)) (:stdout result) (throw (:stderr result))))) ;; ;; estream helpers. ;; (define (es-read-all stream) (let loop ((acc "")) (if (es-feof stream) acc (loop (string-append acc (es-read stream 4096)))))) ;; ;; File management. ;; (define (file-exists? name) (call-with-input-file name (lambda (port) #t))) (define (file=? a b) (file-equal a b #t)) (define (text-file=? a b) (file-equal a b #f)) (define (file-copy from to) (catch '() (unlink to)) (letfd ((source (open from (logior O_RDONLY O_BINARY))) (sink (open to (logior O_WRONLY O_CREAT O_BINARY) #o600))) (splice source sink))) (define (text-file-copy from to) (catch '() (unlink to)) (letfd ((source (open from O_RDONLY)) (sink (open to (logior O_WRONLY O_CREAT) #o600))) (splice source sink))) (define (path-join . components) (let loop ((acc #f) (rest (filter (lambda (s) (not (string=? "" s))) components))) (if (null? rest) acc (loop (if (string? acc) (string-append acc "/" (car rest)) (car rest)) (cdr rest))))) (assert (string=? (path-join "foo" "bar" "baz") "foo/bar/baz")) (assert (string=? (path-join "" "bar" "baz") "bar/baz")) ;; Is PATH an absolute path? (define (absolute-path? path) (or (char=? #\/ (string-ref path 0)) (and *win32* (char=? #\\ (string-ref path 0))) (and *win32* (char-alphabetic? (string-ref path 0)) (char=? #\: (string-ref path 1)) (or (char=? #\/ (string-ref path 2)) (char=? #\\ (string-ref path 2)))))) ;; Make PATH absolute. (define (canonical-path path) (if (absolute-path? path) path (path-join (getcwd) path))) (define (in-srcdir . names) (canonical-path (apply path-join (cons (getenv "abs_top_srcdir") names)))) (define (in-objdir . names) (canonical-path (apply path-join (cons (getenv "objdir") names)))) ;; Split a list of paths. (define (pathsep-split s) (string-split s *pathsep*)) ;; Join a list of paths. (define (pathsep-join paths) (foldr (lambda (a b) (string-append a (string *pathsep*) b)) (car paths) (cdr paths))) ;; Try to find NAME in PATHS. Returns the full path name on success, ;; or raises an error. (define (path-expand name paths) (let loop ((path paths)) (if (null? path) (throw "Could not find" name "in" paths) (let* ((qualified-name (path-join (car path) name)) (file-exists (call-with-input-file qualified-name (lambda (x) #t)))) (if file-exists qualified-name (loop (cdr path))))))) ;; Expand NAME using the gpgscm load path. Use like this: ;; (load (with-path "library.scm")) (define (with-path name) (catch name (path-expand name (pathsep-split (getenv "GPGSCM_PATH"))))) (define (basename path) (let ((i (string-index path #\/))) (if (equal? i #f) path (basename (substring path (+ 1 i) (string-length path)))))) (define (basename-suffix path suffix) (basename (if (string-suffix? path suffix) (substring path 0 (- (string-length path) (string-length suffix))) path))) (define (dirname path) (let ((i (string-rindex path #\/))) (if i (substring path 0 i) "."))) (assert (string=? "foo/bar" (dirname "foo/bar/baz"))) ;; Helper for (pipe). (define :read-end car) (define :write-end cadr) ;; let-like macro that manages file descriptors. ;; ;; (letfd ) ;; ;; Bind all variables given in and initialize each of them ;; to the given initial value, and close them after evaluating . (define-macro (letfd bindings . body) (let bind ((bindings' bindings)) (if (null? bindings') `(begin ,@body) (let* ((binding (car bindings')) (name (car binding)) (initializer (cadr binding))) `(let ((,name ,initializer)) (finally (close ,name) ,(bind (cdr bindings')))))))) (define-macro (with-working-directory new-directory . expressions) (let ((new-dir (gensym)) (old-dir (gensym))) `(let* ((,new-dir ,new-directory) (,old-dir (getcwd))) (dynamic-wind (lambda () (if ,new-dir (chdir ,new-dir))) (lambda () ,@expressions) (lambda () (chdir ,old-dir)))))) ;; Make a temporary directory. If arguments are given, they are ;; joined using path-join, and must end in a component ending in ;; "XXXXXX". If no arguments are given, a suitable location and ;; generic name is used. Returns an absolute path. (define (mkdtemp . components) (canonical-path (_mkdtemp (if (null? components) (path-join (get-temp-path) (string-append "gpgscm-" (get-isotime) "-" (basename-suffix *scriptname* ".scm") "-XXXXXX")) (apply path-join components))))) ;; Make a temporary directory and remove it at interpreter shutdown. ;; Note that there are macros that limit the lifetime of temporary ;; directories and files to a lexical scope. Use those if possible. ;; Otherwise this works like mkdtemp. (define (mkdtemp-autoremove . components) (let ((dir (apply mkdtemp components))) (atexit (lambda () (if (= *exit-status* 0) (unlink-recursively dir)))) dir)) (define-macro (with-temporary-working-directory . expressions) (let ((tmp-sym (gensym))) `(let* ((,tmp-sym (mkdtemp))) (finally (unlink-recursively ,tmp-sym) (with-working-directory ,tmp-sym ,@expressions))))) (define (make-temporary-file . args) (canonical-path (path-join (mkdtemp) (if (null? args) "a" (car args))))) (define (remove-temporary-file filename) (catch '() (unlink filename)) (let ((dirname (substring filename 0 (string-rindex filename #\/)))) (catch (echo "removing temporary directory" dirname "failed") (rmdir dirname)))) ;; let-like macro that manages temporary files. ;; ;; (lettmp ) ;; ;; Bind all variables given in , initialize each of them to ;; a string representing an unique path in the filesystem, and delete ;; them after evaluating . (define-macro (lettmp bindings . body) (let bind ((bindings' bindings)) (if (null? bindings') `(begin ,@body) (let ((name (car bindings')) (rest (cdr bindings'))) `(let ((,name (make-temporary-file ,(symbol->string name)))) (finally (remove-temporary-file ,name) ,(bind rest))))))) (define (check-execution source transformer) (lettmp (sink) (transformer source sink))) (define (check-identity source transformer) (lettmp (sink) (transformer source sink) (if (not (file=? source sink)) (fail "mismatch")))) ;; ;; Monadic pipe support. ;; (define pipeM (package (define (new procs source sink producer) (package (define (dump) (write (list procs source sink producer)) (newline)) - (define (add-proc command pid) - (new (cons (list command pid) procs) source sink producer)) - (define (commands) - (map car procs)) - (define (pids) - (map cadr procs)) + (define (add-proc proc) + (new (cons proc procs) source sink producer)) (define (set-source source') (new procs source' sink producer)) (define (set-sink sink') (new procs source sink' producer)) (define (set-producer producer') (if producer (throw "producer already set")) (new procs source sink producer')))))) +(define (process-wait-list procs hang) + (map (lambda (p) (process-wait p hang)) procs)) + (define (pipe:do . commands) (let loop ((M (pipeM::new '() CLOSED_FD CLOSED_FD #f)) (cmds commands)) (if (null? cmds) (begin (if M::producer (M::producer)) (if (not (null? M::procs)) - (let* ((retcodes (wait-processes (map stringify (M::commands)) - (M::pids) #t)) - (results (map (lambda (p r) (append p (list r))) + (let* ((retcodes (process-wait-list M::procs #t)) + (results (map (lambda (p r) (cons p r)) M::procs retcodes)) - (failed (filter (lambda (x) (not (= 0 (caddr x)))) + (failed (filter (lambda (x) (not (= 0 (cdr x)))) results))) (if (not (null? failed)) (throw failed))))) ; xxx nicer reporting (if (and (= 2 (length cmds)) (number? (cadr cmds))) ;; hack: if it's an fd, use it as sink (let ((M' ((car cmds) (M::set-sink (cadr cmds))))) (if (> M::source 2) (close M::source)) (if (> (cadr cmds) 2) (close (cadr cmds))) (loop M' '())) (let ((M' ((car cmds) M))) (if (> M::source 2) (close M::source)) (loop M' (cdr cmds))))))) (define (pipe:open pathname flags) (lambda (M) (M::set-source (open pathname flags)))) (define (pipe:defer producer) (lambda (M) (let* ((p (outbound-pipe)) (M' (M::set-source (:read-end p)))) (M'::set-producer (lambda () (producer (:write-end p)) (close (:write-end p))))))) (define (pipe:echo data) (pipe:defer (lambda (sink) (display data (fdopen sink "wb"))))) (define (pipe:spawn command) (lambda (M) (define (do-spawn M new-source) - (let ((pid (spawn-process-fd command M::source M::sink - (if (> (*verbose*) 0) - STDERR_FILENO CLOSED_FD))) + (let ((proc (process-spawn-fd command M::source M::sink + (if (> (*verbose*) 0) + STDERR_FILENO CLOSED_FD))) (M' (M::set-source new-source))) - (M'::add-proc command pid))) + (M'::add-proc proc))) (if (= CLOSED_FD M::sink) (let* ((p (pipe)) (M' (do-spawn (M::set-sink (:write-end p)) (:read-end p)))) (close (:write-end p)) (M'::set-sink CLOSED_FD)) (do-spawn M CLOSED_FD)))) (define (pipe:splice sink) (lambda (M) (splice M::source sink) (M::set-source CLOSED_FD))) (define (pipe:write-to pathname flags mode) (open pathname flags mode)) ;; ;; Monadic transformer support. ;; (define (tr:do . commands) (let loop ((tmpfiles '()) (source #f) (cmds commands)) (if (null? cmds) (for-each remove-temporary-file tmpfiles) (let* ((v ((car cmds) tmpfiles source)) (tmpfiles' (car v)) (sink (cadr v)) (error (caddr v))) (if error (begin (for-each remove-temporary-file tmpfiles') (apply throw error))) (loop tmpfiles' sink (cdr cmds)))))) (define (tr:open pathname) (lambda (tmpfiles source) (list tmpfiles pathname #f))) (define (tr:spawn input command) (lambda (tmpfiles source) (if (and (member '**in** command) (not source)) (fail (string-append (stringify cmd) " needs an input"))) (let* ((t (make-temporary-file)) (cmd (map (lambda (x) (cond ((equal? '**in** x) source) ((equal? '**out** x) t) (else x))) command))) (catch (list (cons t tmpfiles) t *error*) (call-popen cmd input) (if (and (member '**out** command) (not (file-exists? t))) (fail (string-append (stringify cmd) " did not produce '" t "'."))) (list (cons t tmpfiles) t #f))))) (define (tr:write-to pathname) (lambda (tmpfiles source) (rename source pathname) (list tmpfiles pathname #f))) (define (tr:pipe-do . commands) (lambda (tmpfiles source) (let ((t (make-temporary-file))) (apply pipe:do `(,@(if source `(,(pipe:open source (logior O_RDONLY O_BINARY))) '()) ,@commands ,(pipe:write-to t (logior O_WRONLY O_BINARY O_CREAT) #o600))) (list (cons t tmpfiles) t #f)))) (define (tr:assert-identity reference) (lambda (tmpfiles source) (if (not (file=? source reference)) (fail "mismatch")) (list tmpfiles source #f))) (define (tr:assert-weak-identity reference) (lambda (tmpfiles source) (if (not (text-file=? source reference)) (fail "mismatch")) (list tmpfiles source #f))) (define (tr:assert-same reference) (lambda (tmpfiles source) (if (not (string=? (call-with-input-file source read-all) reference)) (fail "mismatch")) (list tmpfiles source #f))) (define (tr:call-with-content function . args) (lambda (tmpfiles source) (catch (list tmpfiles source *error*) (apply function `(,(call-with-input-file source read-all) ,@args))) (list tmpfiles source #f))) ;; ;; Developing and debugging tests. ;; ;; Spawn an os shell. (define (interactive-shell) (call-with-fds `(,(getenv "SHELL") -i) 0 1 2)) ;; ;; The main test framework. ;; (define semaphore (package (define (new n) (package (define (acquire!?) (if (> n 0) (begin (set! n (- n 1)) #t) #f)) (define (release!) (set! n (+ n 1))))))) ;; A pool of tests. (define test-pool (package (define (new n) (package ;; A semaphore to restrict the number of spawned processes. (define sem (semaphore::new n)) ;; A list of enqueued, but not yet run tests. (define enqueued '()) ;; A list of running or finished processes. (define procs '()) (define (add test) (if (test::started?) (set! procs (cons test procs)) (if (sem::acquire!?) (add (test::run-async)) (set! enqueued (cons test enqueued)))) (current-environment)) ;; Pop the last of the enqueued tests off the fifo queue. (define (pop-test!) (let ((i (length enqueued))) (assert (> i 0)) (cond ((= i 1) (let ((test (car enqueued))) (set! enqueued '()) test)) (else (let* ((tail (list-tail enqueued (- i 2))) (test (cadr tail))) (set-cdr! tail '()) (assert (= (length enqueued) (- i 1))) test))))) - (define (pid->test pid) - (let ((t (filter (lambda (x) (= pid x::pid)) procs))) + (define (proc->test proc) + (let ((t (filter (lambda (x) (eq? proc x::proc)) procs))) (if (null? t) #f (car t)))) (define (wait) (if (null? enqueued) ;; If no tests are enqueued, we can just block until all ;; of them finished. (wait' #t) ;; Otherwise, we must not block, but give some tests the ;; chance to finish so that we can start new ones. (begin (wait' #f) (usleep (/ 1000000 10)) (wait)))) (define (wait' hang) (let ((unfinished (filter (lambda (t) (not t::retcode)) procs))) (if (null? unfinished) (current-environment) (let ((names (map (lambda (t) t::name) unfinished)) - (pids (map (lambda (t) t::pid) unfinished)) + (procs (map (lambda (t) t::proc) unfinished)) (any #f)) (for-each (lambda (test retcode) (unless (< retcode 0) (test::set-end-time!) (test:::set! 'retcode retcode) (test::report) (sem::release!) (set! any #t))) - (map pid->test pids) - (wait-processes (map stringify names) pids hang)) + (map proc->test procs) + (process-wait-list procs hang)) ;; If some processes finished, try to start new ones. (let loop () (cond ((not any) #f) ((pair? enqueued) (if (sem::acquire!?) (let ((test (pop-test!))) (add (test::run-async)) (loop))))))))) (current-environment)) (define (filter-tests status) (filter (lambda (p) (eq? status (p::status))) procs)) (define (report) (define (print-tests tests message) (unless (null? tests) (apply echo (cons message (map (lambda (t) t::name) tests))))) (let ((failed (filter-tests 'FAIL)) (xfailed (filter-tests 'XFAIL)) (xpassed (filter-tests 'XPASS)) (skipped (filter-tests 'SKIP))) (echo "===================") (echo (length procs) "tests run," (length (filter-tests 'PASS)) "succeeded," (length failed) "failed," (length xfailed) "failed expectedly," (length xpassed) "succeeded unexpectedly," (length skipped) "skipped.") (print-tests failed "Failed tests:") (print-tests xfailed "Expectedly failed tests:") (print-tests xpassed "Unexpectedly passed tests:") (print-tests skipped "Skipped tests:") (echo "===================") (+ (length failed) (length xpassed)))) (define (xml) (xx::document (xx::tag 'testsuites `((xmlns:xsi "http://www.w3.org/2001/XMLSchema-instance") ("xsi:noNamespaceSchemaLocation" "https://windyroad.com.au/dl/Open%20Source/JUnit.xsd")) (map (lambda (t) (t::xml)) procs)))))))) (define (verbosity n) (if (= 0 n) '() (cons '--verbose (verbosity (- n 1))))) (define (locate-test path) (if (absolute-path? path) path (in-srcdir path))) ;; A single test. (define test (begin ;; Private definitions. (define (isotime->junit t) "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}" "20170418T145809" (string-append (substring t 0 4) "-" (substring t 4 6) "-" (substring t 6 11) ":" (substring t 11 13) ":" (substring t 13 15))) ;; If a tests name ends with a bang (!), it is expected to fail. (define (expect-failure? name) (string-suffix? name "!")) ;; Strips the bang (if any). (define (test-name name) (if (expect-failure? name) (substring name 0 (- (string-length name) 1)) name)) (package (define (scm setup variant name path . args) ;; Start the process. (define (spawn-scm args' in out err) - (spawn-process-fd `(,*argv0* ,@(verbosity (*verbose*)) + (process-spawn-fd `(,*argv0* ,@(verbosity (*verbose*)) ,(locate-test (test-name path)) ,@(if setup (force setup) '()) ,@args' ,@args) in out err)) (new variant name #f spawn-scm #f #f CLOSED_FD (expect-failure? name))) (define (binary setup name path . args) ;; Start the process. (define (spawn-binary args' in out err) - (spawn-process-fd `(,(test-name path) + (process-spawn-fd `(,(test-name path) ,@(if setup (force setup) '()) ,@args' ,@args) in out err)) (new #f name #f spawn-binary #f #f CLOSED_FD (expect-failure? name))) - (define (new variant name directory spawn pid retcode logfd expect-failure) + (define (new variant name directory spawn proc retcode logfd expect-failure) (package ;; XXX: OO glue. (define self (current-environment)) (define (:set! key value) (eval `(set! ,key ,value) (current-environment)) (current-environment)) ;; The log is written here. (define log-file-name #f) ;; Record time stamps. (define timestamp #f) (define start-time 0) (define end-time 0) (define (set-start-time!) (set! timestamp (isotime->junit (get-isotime))) (set! start-time (get-time))) (define (set-end-time!) (set! end-time (get-time))) ;; Has the test been started yet? (define (started?) - (number? pid)) + proc) (define (open-log-file) (unless log-file-name (set! log-file-name (path-join (getenv "objdir") (if variant (string-append name "." variant ".log") (string-append name ".log"))))) (catch '() (unlink log-file-name)) (open log-file-name (logior O_RDWR O_BINARY O_CREAT) #o600)) (define (run-sync . args) (set-start-time!) (letfd ((log (open-log-file))) (with-working-directory directory (let* ((p (inbound-pipe)) - (pid' (spawn args 0 (:write-end p) (:write-end p)))) + (proc' (spawn args 0 (:write-end p) (:write-end p)))) (close (:write-end p)) (splice (:read-end p) STDERR_FILENO log) (close (:read-end p)) - (set! pid pid') - (set! retcode (wait-process name pid' #t))))) + (set! proc proc') + (set! retcode (process-wait proc' #t))))) (report) (current-environment)) (define (run-sync-quiet . args) (set-start-time!) (with-working-directory directory - (set! pid (spawn args CLOSED_FD CLOSED_FD CLOSED_FD))) - (set! retcode (wait-process name pid #t)) + (set! proc (spawn args CLOSED_FD CLOSED_FD CLOSED_FD))) + (set! retcode (process-wait proc #t)) (set-end-time!) (current-environment)) (define (run-async . args) (set-start-time!) (let ((log (open-log-file))) (with-working-directory directory - (set! pid (spawn args CLOSED_FD log log))) + (set! proc (spawn args CLOSED_FD log log))) (set! logfd log)) (current-environment)) (define (status) (let* ((t' (assoc retcode '((0 PASS) (77 SKIP) (99 ERROR)))) (t (if (not t') 'FAIL (cadr t')))) (if expect-failure (case t ((PASS) 'XPASS) ((FAIL) 'XFAIL) (else t)) t))) (define (status-string) (cadr (assoc (status) '((PASS "PASS") (SKIP "SKIP") (ERROR "ERROR") (FAIL "FAIL") (XPASS "XPASS") (XFAIL "XFAIL"))))) (define (report) (unless (= logfd CLOSED_FD) (seek logfd 0 SEEK_SET) (splice logfd STDERR_FILENO) (close logfd)) (echo (string-append (status-string) ":") (if variant (string-append "<" variant ">" name) name))) (define (xml) (xx::tag 'testsuite `((name ,name) (time ,(- end-time start-time)) (package ,(dirname name)) (id 0) (timestamp ,timestamp) (hostname "unknown") (tests 1) (failures ,(if (eq? FAIL (status)) 1 0)) (errors ,(if (eq? ERROR (status)) 1 0))) (list (xx::tag 'properties) (xx::tag 'testcase `((name ,(basename name)) (classname ,(string-translate (dirname name) "/" ".")) (time ,(- end-time start-time))) `(,@(case (status) ((PASS XFAIL) '()) ((SKIP) (list (xx::tag 'skipped))) ((ERROR) (list (xx::tag 'error '((message "Unknown error."))))) (else (list (xx::tag 'failure '((message "Unknown error.")))))))) (xx::tag 'system-out '() (list (xx::textnode (read-all (open-input-file log-file-name))))) (xx::tag 'system-err '() (list (xx::textnode ""))))))))))) ;; Run the setup target to create an environment, then run all given ;; tests in parallel. (define (run-tests-parallel tests n) (let loop ((pool (test-pool::new n)) (tests' tests)) (if (null? tests') (let ((results (pool::wait))) ((results::xml) (open-output-file "report.xml")) (exit (results::report))) (let ((wd (mkdtemp-autoremove)) (test (car tests'))) (test:::set! 'directory wd) (loop (pool::add test) (cdr tests')))))) ;; Run the setup target to create an environment, then run all given ;; tests in sequence. (define (run-tests-sequential tests) (let loop ((pool (test-pool::new 1)) (tests' tests)) (if (null? tests') (let ((results (pool::wait))) ((results::xml) (open-output-file "report.xml")) (exit (results::report))) (let ((wd (mkdtemp-autoremove)) (test (car tests'))) (test:::set! 'directory wd) (loop (pool::add (test::run-sync)) (cdr tests')))))) ;; Run tests either in sequence or in parallel, depending on the ;; number of tests and the command line flags. (define (run-tests tests) (let ((parallel (flag "--parallel" *args*)) (default-parallel-jobs 32)) (if (and parallel (> (length tests) 1)) (run-tests-parallel tests (if (and (pair? parallel) (string->number (car parallel))) (string->number (car parallel)) default-parallel-jobs)) (run-tests-sequential tests)))) ;; Load all tests from the given path. (define (load-tests . path) (load (apply in-srcdir `(,@path "all-tests.scm"))) all-tests) ;; Helper to create environment caches from test functions. SETUP ;; must be a test implementing the producer side cache protocol. ;; Returns a promise containing the arguments that must be passed to a ;; test implementing the consumer side of the cache protocol. (define (make-environment-cache setup) (delay (with-temporary-working-directory (let ((tarball (make-temporary-file "environment-cache"))) (atexit (lambda () (remove-temporary-file tarball))) (setup::run-sync '--create-tarball tarball) (if (not (equal? 'PASS (setup::status))) (fail "Setup failed.")) `(--unpack-tarball ,tarball))))) ;; Command line flag handling. Returns the elements following KEY in ;; ARGUMENTS up to the next argument, or #f if KEY is not in ;; ARGUMENTS. If 'KEY=XYZ' is encountered, then the singleton list ;; containing 'XYZ' is returned. (define (flag key arguments) (cond ((null? arguments) #f) ((string=? key (car arguments)) (let loop ((acc '()) (args (cdr arguments))) (if (or (null? args) (string-prefix? (car args) "--")) (reverse acc) (loop (cons (car args) acc) (cdr args))))) ((string-prefix? (car arguments) (string-append key "=")) (list (substring (car arguments) (+ (string-length key) 1) (string-length (car arguments))))) ((string=? "--" (car arguments)) #f) (else (flag key (cdr arguments))))) (assert (equal? (flag "--xxx" '("--yyy")) #f)) (assert (equal? (flag "--xxx" '("--xxx")) '())) (assert (equal? (flag "--xxx" '("--xxx" "yyy")) '("yyy"))) (assert (equal? (flag "--xxx" '("--xxx=foo" "yyy")) '("foo"))) (assert (equal? (flag "--xxx" '("--xxx" "yyy" "zzz")) '("yyy" "zzz"))) (assert (equal? (flag "--xxx" '("--xxx" "yyy" "zzz" "--")) '("yyy" "zzz"))) (assert (equal? (flag "--xxx" '("--xxx" "yyy" "--" "zzz")) '("yyy"))) (assert (equal? (flag "--" '("--" "xxx" "yyy" "--" "zzz")) '("xxx" "yyy"))) diff --git a/tests/openpgp/defs.scm b/tests/openpgp/defs.scm index bf3714f50..1ac25bf65 100644 --- a/tests/openpgp/defs.scm +++ b/tests/openpgp/defs.scm @@ -1,532 +1,533 @@ ;; Common definitions for the OpenPGP test scripts. ;; ;; Copyright (C) 2016, 2017 g10 Code GmbH ;; ;; This file is part of GnuPG. ;; ;; GnuPG is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 3 of the License, or ;; (at your option) any later version. ;; ;; GnuPG is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program; if not, see . ;; ;; Constants. ;; (define usrname1 "one@example.com") (define usrpass1 "def") (define usrname2 "two@example.com") (define usrpass2 "") (define usrname3 "three@example.com") (define usrpass3 "") (define dsa-usrname1 "pgp5") ;; we use the sub key because we do not yet have the logic to derive ;; the first encryption key from a keyblock (I guess) (Well of course ;; we have this by now and the notation below will lookup the primary ;; first and then search for the encryption subkey.) (define dsa-usrname2 "0xCB879DE9") (define keys (package (define (new fpr grip uids subkeys) (package)) (define (subkey fpr grip) (package)) (define alfa (new "A0FF4590BB6122EDEF6E3C542D727CC768697734" "76F7E2B35832976B50A27A282D9B87E44577EB66" '("alfa@example.net" "alpha@example.net") (list (subkey "3B3FBC948FE59301ED629EFB6AE6D7EE46A871F8" "A0747D5F9425E6664F4FFBEED20FBCA79FDED2BD")))) (define one (new "289B0EF1D105E124B6F626020EF77096D74C5F22" "50B2D4FA4122C212611048BC5FC31BD44393626E" '("one@example.com") (list (subkey "EB467DCA4AD7676A6A62B2ABABAB28A247BE2775" "7E201E28B6FEB2927B321F443205F4724EBE637E")))) (define two (new "C1DEBB34EA8B71009EAFA474973D50E1C40FDECF" "343D8AF79796EE107D645A2787A9D9252F924E6F" '("two@example.com") (list (subkey "CD3D0F5701CBFCACB2A4907305A37887B27907AA" "8B5ABF3EF9EB8D96B91A0B8C2C4401C91C834C34")))))) (define key-file1 "samplekeys/rsa-rsa-sample-1.asc") (define key-file2 "samplekeys/ed25519-cv25519-sample-1.asc") (define plain-files '("plain-1" "plain-2" "plain-3" "plain-large")) (define data-files '("data-500" "data-9000" "data-32000" "data-80000")) (define exp-files '()) (define all-files (append plain-files data-files)) (let ((verbose (string->number (getenv "verbose")))) (if (number? verbose) (*set-verbose!* verbose))) (define (qualify executable) (string-append executable (getenv "EXEEXT"))) (define (getenv' key default) (let ((value (getenv key))) (if (string=? "" value) default value))) (define (percent-decode s) (define (decode c) (if (and (> (length c) 2) (char=? #\% (car c))) (integer->char (string->number (string #\# #\x (cadr c) (caddr c)))) #f)) (let loop ((i 0) (c (string->list s)) (r (make-string (string-length s)))) (if (null? c) (substring r 0 i) (let ((decoded (decode c))) (string-set! r i (if decoded decoded (car c))) (loop (+ 1 i) (if decoded (cdddr c) (cdr c)) r))))) (assert (equal? (percent-decode "") "")) (assert (equal? (percent-decode "%61") "a")) (assert (equal? (percent-decode "foob%61r") "foobar")) (define (percent-encode s) (define (encode c) `(#\% ,@(string->list (number->string (char->integer c) 16)))) (let loop ((acc '()) (cs (reverse (string->list s)))) (if (null? cs) (list->string acc) (case (car cs) ((#\: #\%) (loop (append (encode (car cs)) acc) (cdr cs))) (else (loop (cons (car cs) acc) (cdr cs))))))) (assert (equal? (percent-encode "") "")) (assert (equal? (percent-encode "%61") "%2561")) (assert (equal? (percent-encode "foob%61r") "foob%2561r")) ;; Note that the entry for pinentry relies on the fact that ;; GNUPG_BUILD_ROOT is the top of the build root. The second element ;; in each list is an envvar which can be used to specifiy a different ;; tool than the installed one. (define tools '((gpgv "GPGV" "g10/gpgv") (gpg-connect-agent "GPG_CONNECT_AGENT" "tools/gpg-connect-agent") (gpgconf "GPGCONF" "tools/gpgconf") (gpg-preset-passphrase "GPG_PRESET_PASSPHRASE" "agent/gpg-preset-passphrase") (gpgtar "GPGTAR" "tools/gpgtar") (pinentry "PINENTRY" "tests/openpgp/fake-pinentry"))) (define (tool-hardcoded which) (let ((t (assoc which tools))) (getenv' (cadr t) (qualify (string-append (getenv "GNUPG_BUILD_ROOT") "/" (caddr t)))))) (define with-valgrind? (not (string=? (getenv "with_valgrind") ""))) ;; You can splice VALGRIND into your argument vector to run programs ;; under valgrind. For example, to run valgrind on gpg, you may want ;; to redefine gpg: ;; ;; (set! gpg `(,@valgrind ,@gpg)) ;; (define valgrind '("/usr/bin/valgrind" -q --leak-check=no --track-origins=yes --error-exitcode=154 --exit-on-first-error=yes)) (define (gpg-conf . args) (gpg-conf' "" args)) (define (gpg-conf' input args) (let ((s (call-popen `(,(tool-hardcoded 'gpgconf) ,@(if *win32* (list '--build-prefix (getenv "objdir")) '()) ,@args) input))) (map (lambda (line) (map percent-decode (string-split line #\:))) (string-split-newlines s)))) (define :gc:c:name car) (define :gc:c:description cadr) (define :gc:c:pgmname caddr) (define (:gc:o:name x) (list-ref x 0)) (define (:gc:o:flags x) (string->number (list-ref x 1))) (define (:gc:o:level x) (string->number (list-ref x 2))) (define (:gc:o:description x) (list-ref x 3)) (define (:gc:o:type x) (string->number (list-ref x 4))) (define (:gc:o:alternate-type x) (string->number (list-ref x 5))) (define (:gc:o:argument-name x) (list-ref x 6)) (define (:gc:o:default-value x) (list-ref x 7)) (define (:gc:o:default-argument x) (list-ref x 8)) (define (:gc:o:value x) (if (< (length x) 10) "" (list-ref x 9))) (define (gpg-config component key) (package (define (value) (let* ((conf (assoc key (gpg-conf '--list-options component))) (type (:gc:o:type conf)) (value (:gc:o:value conf))) (case type ((0 2 3) (string->number value)) ((1 32) (substring value 1 (string-length value)))))) (define (update value) (let ((value' (cond ((string? value) (string-append "\"" value)) ((number? value) (number->string value)) (else (throw "Unsupported value" value))))) (gpg-conf' (string-append key ":0:" (percent-encode value')) `(--change-options ,component)))) (define (clear) (gpg-conf' (string-append key ":16:") `(--change-options ,component))))) (define gpg-components (apply gpg-conf '(--list-components))) (define (tool which) (case which ((gpg gpg-agent scdaemon gpgsm keyboxd dirmngr) (:gc:c:pgmname (assoc (symbol->string which) gpg-components))) (else (tool-hardcoded which)))) (define (gpg-has-option? option) (string-contains? (call-popen `(,(tool 'gpg) --dump-options) "") option)) (define have-opt-always-trust (catch #f (with-ephemeral-home-directory (lambda ()) (lambda ()) (call-check `(,(tool 'gpg) --gpgconf-test --always-trust))) #t)) (define GPG `(,(tool 'gpg) --no-permission-warning ,@(if have-opt-always-trust '(--always-trust) '()))) (define GPGV `(,(tool 'gpgv))) (define PINENTRY (tool 'pinentry)) (define (tr:gpg input args) (tr:spawn input `(,@GPG --output **out** ,@args **in**))) (define (tr:gpgstatus input args) (tr:spawn input `(,@GPG --output dummy --status-file **out** ,@args **in**))) (define (pipe:gpg args) (pipe:spawn `(,@GPG --output - ,@args))) (define (gpg-with-colons args) (let ((s (call-popen `(,@GPG --with-colons ,@args) ""))) (map (lambda (line) (string-split line #\:)) (string-split-newlines s)))) ;; Convenient accessors for the colon output. (define (:type x) (string->symbol (list-ref x 0))) (define (:length x) (string->number (list-ref x 2))) (define (:alg x) (string->number (list-ref x 3))) (define (:expire x) (list-ref x 6)) (define (:fpr x) (list-ref x 9)) (define (:cap x) (list-ref x 11)) (define (have-public-key? key) (catch #f (pair? (filter (lambda (l) (and (equal? 'fpr (:type l)) (equal? key::fpr (:fpr l)))) (gpg-with-colons `(--list-keys ,key::fpr)))))) (define (have-secret-key? key) (catch #f (pair? (filter (lambda (l) (and (equal? 'fpr (:type l)) (equal? key::fpr (:fpr l)))) (gpg-with-colons `(--list-secret-keys ,key::fpr)))))) (define (have-secret-key-file? key) (file-exists? (path-join (getenv "GNUPGHOME") "private-keys-v1.d" (string-append key::grip ".key")))) (define (get-config what) (string-split (caddar (gpg-with-colons `(--list-config ,what))) #\;)) (define all-pubkey-algos (delay (get-config "pubkeyname"))) (define all-hash-algos (delay (get-config "digestname"))) (define all-cipher-algos (delay (get-config "ciphername"))) (define all-compression-algos (delay (get-config "compressname"))) (define (have-pubkey-algo? x) (not (not (member x (force all-pubkey-algos))))) (define (have-hash-algo? x) (not (not (member x (force all-hash-algos))))) (define (have-cipher-algo? x) (not (not (member x (force all-cipher-algos))))) (define (have-compression-algo? x) (not (not (member x (force all-compression-algos))))) (define (gpg-pipe args0 args1 errfd) (lambda (source sink) (let* ((p (pipe)) - (task0 (spawn-process-fd `(,@GPG ,@args0) + (task0 (process-spawn-fd `(,@GPG ,@args0) source (:write-end p) errfd)) (_ (close (:write-end p))) - (task1 (spawn-process-fd `(,@GPG ,@args1) + (task1 (process-spawn-fd `(,@GPG ,@args1) (:read-end p) sink errfd))) (close (:read-end p)) - (wait-processes (list GPG GPG) (list task0 task1) #t)))) + (process-wait task0 #t) + (process-wait task1 #t)))) (setenv "GPG_AGENT_INFO" "" #t) (setenv "GNUPGHOME" (getcwd) #t) (define GNUPGHOME (getcwd)) ;; ;; GnuPG helper. ;; ;; Call GPG to obtain the hash sums. Either specify an input file in ;; ARGS, or an string in INPUT. Returns a list of ( ;; "") lists. (define (gpg-hash-string args input) (map (lambda (line) (let ((p (string-split line #\:))) (list (string->number (cadr p)) (caddr p)))) (string-split-newlines (call-popen `(,@GPG --with-colons ,@args) input)))) ;; Dearmor a file. (define (dearmor source-name sink-name) (pipe:do (pipe:open source-name (logior O_RDONLY O_BINARY)) (pipe:spawn `(,@GPG --dearmor)) (pipe:write-to sink-name (logior O_WRONLY O_CREAT O_BINARY) #o600))) (define (gpg-dump-packets source-name sink-name) (pipe:do (pipe:open source-name (logior O_RDONLY O_BINARY)) (pipe:spawn `(,@GPG --list-packets)) (pipe:write-to sink-name (logior O_WRONLY O_CREAT O_BINARY) #o600))) ;; ;; Support for test environment creation and teardown. ;; (define (make-test-data filename size) (call-with-binary-output-file filename (lambda (port) (display (make-random-string size) port)))) (define (create-file name . lines) (catch #f (unlink name)) (letfd ((fd (open name (logior O_WRONLY O_CREAT O_BINARY) #o600))) (let ((port (fdopen fd "wb"))) (for-each (lambda (line) (display line port) (newline port)) lines)))) (define (create-gpghome) (log "Creating test environment...") (srandom (getpid)) (make-test-data "random_seed" 600) (log "Creating configuration files") (if (flag "--use-keyring" *args*) (create-file "pubring.gpg")) (create-file "common.conf" (if (flag "--use-keyboxd" *args*) "use-keyboxd" "#use-keyboxd") (string-append "keyboxd-program " (tool 'keyboxd)) ) (create-file "gpg.conf" ;;"log-file socket:///tmp/S.wklog" ;;"verbose" "no-greeting" "no-secmem-warning" "no-permission-warning" "batch" "no-auto-key-retrieve" "no-auto-key-locate" "allow-weak-digest-algos" "allow-old-cipher-algos" "ignore-mdc-error" (if have-opt-always-trust "no-auto-check-trustdb" "#no-auto-check-trustdb") (string-append "agent-program " (tool 'gpg-agent) "|--debug-quick-random\n") ) (create-file "keyboxd.conf" ;;"log-file socket:///tmp/S.wklog" ;;"verbose" ;;"debug ipc" ) (create-file "gpg-agent.conf" "allow-preset-passphrase" "no-grab" "enable-ssh-support" "s2k-count 65536" (string-append "pinentry-program " (tool 'pinentry)) "disable-scdaemon")) ;; Initialize the test environment, install appropriate configuration ;; and start the agent, without any keys. (define (setup-environment) (create-gpghome) (start-agent)) (define (setup-environment-no-atexit) (create-gpghome) (start-agent #t)) (define (create-sample-files) (log "Creating sample data files") (for-each (lambda (size) (make-test-data (string-append "data-" (number->string size)) size)) '(500 9000 32000 80000)) (log "Unpacking samples") (for-each (lambda (name) (dearmor (in-srcdir "tests" "openpgp" (string-append name "o.asc")) name)) plain-files)) (define (create-legacy-gpghome) (create-sample-files) (log "Storing private keys") (for-each (lambda (name) (dearmor (in-srcdir "tests" "openpgp" "privkeys" (string-append name ".asc")) (string-append "private-keys-v1.d/" name ".key"))) '("50B2D4FA4122C212611048BC5FC31BD44393626E" "7E201E28B6FEB2927B321F443205F4724EBE637E" "13FDB8809B17C5547779F9D205C45F47CE0217CE" "343D8AF79796EE107D645A2787A9D9252F924E6F" "8B5ABF3EF9EB8D96B91A0B8C2C4401C91C834C34" "0D6F6AD4C4C803B25470F9104E9F4E6A4CA64255" "FD692BD59D6640A84C8422573D469F84F3B98E53" "76F7E2B35832976B50A27A282D9B87E44577EB66" "A0747D5F9425E6664F4FFBEED20FBCA79FDED2BD" "00FE67F28A52A8AA08FFAED20AF832DA916D1985" "1DF48228FEFF3EC2481B106E0ACA8C465C662CC5" "A2832820DC9F40751BDCD375BB0945BA33EC6B4C" "ADE710D74409777B7729A7653373D820F67892E0" "CEFC51AF91F68A2904FBFF62C4F075A4785B803F" "1E28F20E41B54C2D1234D896096495FF57E08D18" "EB33B687EB8581AB64D04852A54453E85F3DF62D" "C6A6390E9388CDBAD71EAEA698233FE5E04F001E" "D69102E0F5AC6B6DB8E4D16DA8E18CF46D88CAE3")) (for-each (lambda (name) (file-copy (in-srcdir "tests" "openpgp" "privkeys" (string-append name ".key")) (string-append "private-keys-v1.d/" name ".key"))) '("891067FFFC6D67D37BD4BFC399191C5F3989D1B5" "F27FC04CB01723A4CB6F5399F7B86CCD82C0169C")) (log "Importing public demo and test keys") (for-each (lambda (file) (call-check `(,@GPG --yes --import ,(in-srcdir "tests" "openpgp" file)))) (list "pubdemo.asc" "pubring.asc" key-file1)) (pipe:do (pipe:open (in-srcdir "tests" "openpgp" "pubring.pkr.asc") (logior O_RDONLY O_BINARY)) (pipe:spawn `(,@GPG --dearmor)) (pipe:spawn `(,@GPG --yes --import)))) (define (preset-passphrases) (log "Presetting passphrases") ;; one@example.com (call-check `(,(tool 'gpg-preset-passphrase) --preset --passphrase def "50B2D4FA4122C212611048BC5FC31BD44393626E")) (call-check `(,(tool 'gpg-preset-passphrase) --preset --passphrase def "7E201E28B6FEB2927B321F443205F4724EBE637E")) ;; alpha@example.net (call-check `(,(tool 'gpg-preset-passphrase) --preset --passphrase abc "76F7E2B35832976B50A27A282D9B87E44577EB66")) (call-check `(,(tool 'gpg-preset-passphrase) --preset --passphrase abc "A0747D5F9425E6664F4FFBEED20FBCA79FDED2BD"))) ;; Initialize the test environment, install appropriate configuration ;; and start the agent, with the keys from the legacy test suite. (define (setup-legacy-environment) (create-gpghome) (if (member "--unpack-tarball" *args*) (begin (call-check `(,(tool 'gpgtar) --extract --directory=. ,(cadr *args*))) (start-agent)) (begin (start-agent) (create-legacy-gpghome))) (preset-passphrases)) ;; Create the socket dir and start the agent. (define (start-agent . args) (log "Starting gpg-agent...") (let ((gnupghome (getenv "GNUPGHOME"))) (if (null? args) (atexit (lambda () (with-home-directory gnupghome (stop-agent)))))) (catch (log "Warning: Creating socket directory failed:" (car *error*)) (gpg-conf '--create-socketdir)) (call-check `(,(tool 'gpg-connect-agent) --verbose ,(string-append "--agent-program=" (tool 'gpg-agent) "|--debug-quick-random") /bye))) ;; Stop the agent and other daemons and remove the socket dir. (define (stop-agent) (log "Stopping gpg-agent...") (gpg-conf '--kill 'all) (catch (log "Warning: Removing socket directory failed.") (gpg-conf '--remove-socketdir))) ;; Get the trust level for KEYID. Any remaining arguments are simply ;; passed to GPG. ;; ;; This function only supports keys with a single user id. (define (gettrust keyid . args) (let ((trust (list-ref (assoc "pub" (gpg-with-colons `(,@args --list-keys ,keyid))) 1))) (unless (and (= 1 (string-length trust)) (member (string-ref trust 0) (string->list "oidreqnmfuws-"))) (fail "Bad trust value:" trust)) trust)) ;; Check that KEYID's trust level matches EXPECTED-TRUST. Any ;; remaining arguments are simply passed to GPG. ;; ;; This function only supports keys with a single user id. (define (checktrust keyid expected-trust . args) (let ((trust (apply gettrust `(,keyid ,@args)))) (unless (string=? trust expected-trust) (fail keyid ": Expected trust to be" expected-trust "but got" trust)))) ;; ;; Enable checking with valgrind if the envvar "with_valgrind" is set ;; (when with-valgrind? (set! gpg `(,@valgrind ,@gpg))) ;;(set! *args* (append *args* (list "--use-keyboxd"))) ;; end diff --git a/tests/tpm2dtests/defs.scm b/tests/tpm2dtests/defs.scm index 0fef71806..a913818f5 100644 --- a/tests/tpm2dtests/defs.scm +++ b/tests/tpm2dtests/defs.scm @@ -1,464 +1,465 @@ ;; Common definitions for the OpenPGP test scripts. ;; ;; Copyright (C) 2016, 2017 g10 Code GmbH ;; ;; This file is part of GnuPG. ;; ;; GnuPG is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 3 of the License, or ;; (at your option) any later version. ;; ;; GnuPG is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program; if not, see . (let ((verbose (string->number (getenv "verbose")))) (if (number? verbose) (*set-verbose!* verbose))) (define (qualify executable) (string-append executable (getenv "EXEEXT"))) (define (getenv' key default) (let ((value (getenv key))) (if (string=? "" value) default value))) (define (percent-decode s) (define (decode c) (if (and (> (length c) 2) (char=? #\% (car c))) (integer->char (string->number (string #\# #\x (cadr c) (caddr c)))) #f)) (let loop ((i 0) (c (string->list s)) (r (make-string (string-length s)))) (if (null? c) (substring r 0 i) (let ((decoded (decode c))) (string-set! r i (if decoded decoded (car c))) (loop (+ 1 i) (if decoded (cdddr c) (cdr c)) r))))) (assert (equal? (percent-decode "") "")) (assert (equal? (percent-decode "%61") "a")) (assert (equal? (percent-decode "foob%61r") "foobar")) (define (percent-encode s) (define (encode c) `(#\% ,@(string->list (number->string (char->integer c) 16)))) (let loop ((acc '()) (cs (reverse (string->list s)))) (if (null? cs) (list->string acc) (case (car cs) ((#\: #\%) (loop (append (encode (car cs)) acc) (cdr cs))) (else (loop (cons (car cs) acc) (cdr cs))))))) (assert (equal? (percent-encode "") "")) (assert (equal? (percent-encode "%61") "%2561")) (assert (equal? (percent-encode "foob%61r") "foob%2561r")) (define tools '((gpgv "GPGV" "g10/gpgv") (gpg-connect-agent "GPG_CONNECT_AGENT" "tools/gpg-connect-agent") (gpgconf "GPGCONF" "tools/gpgconf") (gpg-preset-passphrase "GPG_PRESET_PASSPHRASE" "agent/gpg-preset-passphrase") (gpgtar "GPGTAR" "tools/gpgtar") (tpm2daemon "TPM2DAEMON" "tpm2d/tpm2daemon") (pinentry "PINENTRY" "tests/openpgp/fake-pinentry"))) (define with-valgrind? (not (string=? (getenv "with_valgrind") ""))) (define (tool-hardcoded which) (let ((t (assoc which tools))) (getenv' (cadr t) (qualify (string-append (getenv "GNUPG_BUILD_ROOT") "/" (caddr t)))))) ;; You can splice VALGRIND into your argument vector to run programs ;; under valgrind. For example, to run valgrind on gpg, you may want ;; to redefine gpg: ;; ;; (set! gpg `(,@valgrind ,@gpg)) ;; (define valgrind '("/usr/bin/valgrind" -q --leak-check=no --track-origins=yes --error-exitcode=154 --exit-on-first-error=yes)) (define (gpg-conf . args) (gpg-conf' "" args)) (define (gpg-conf' input args) (let ((s (call-popen `(,(tool-hardcoded 'gpgconf) ,@args) input))) (map (lambda (line) (map percent-decode (string-split line #\:))) (string-split-newlines s)))) (define :gc:c:name car) (define :gc:c:description cadr) (define :gc:c:pgmname caddr) (define (:gc:o:name x) (list-ref x 0)) (define (:gc:o:flags x) (string->number (list-ref x 1))) (define (:gc:o:level x) (string->number (list-ref x 2))) (define (:gc:o:description x) (list-ref x 3)) (define (:gc:o:type x) (string->number (list-ref x 4))) (define (:gc:o:alternate-type x) (string->number (list-ref x 5))) (define (:gc:o:argument-name x) (list-ref x 6)) (define (:gc:o:default-value x) (list-ref x 7)) (define (:gc:o:default-argument x) (list-ref x 8)) (define (:gc:o:value x) (if (< (length x) 10) "" (list-ref x 9))) (define (gpg-config component key) (package (define (value) (let* ((conf (assoc key (gpg-conf '--list-options component))) (type (:gc:o:type conf)) (value (:gc:o:value conf))) (case type ((0 2 3) (string->number value)) ((1 32) (substring value 1 (string-length value)))))) (define (update value) (let ((value' (cond ((string? value) (string-append "\"" value)) ((number? value) (number->string value)) (else (throw "Unsupported value" value))))) (gpg-conf' (string-append key ":0:" (percent-encode value')) `(--change-options ,component)))) (define (clear) (gpg-conf' (string-append key ":16:") `(--change-options ,component))))) (define gpg-components (apply gpg-conf '(--list-components))) (define (tool which) (case which ((gpg gpg-agent scdaemon gpgsm dirmngr) (:gc:c:pgmname (assoc (symbol->string which) gpg-components))) (else (tool-hardcoded which)))) (define (gpg-has-option? option) (string-contains? (call-popen `(,(tool 'gpg) --dump-options) "") option)) (define have-opt-always-trust (catch #f (with-ephemeral-home-directory (lambda ()) (lambda ()) (call-check `(,(tool 'gpg) --gpgconf-test --always-trust))) #t)) (define GPG `(,(tool 'gpg) --no-permission-warning ,@(if have-opt-always-trust '(--always-trust) '()))) (define GPGV `(,(tool 'gpgv))) (define PINENTRY (tool 'pinentry)) (define TPM2DAEMON (tool 'tpm2daemon)) (define (tr:gpg input args) (tr:spawn input `(,@GPG --output **out** ,@args **in**))) (define (pipe:gpg args) (pipe:spawn `(,@GPG --output - ,@args))) (define (gpg-with-colons args) (let ((s (call-popen `(,@GPG --with-colons ,@args) ""))) (map (lambda (line) (string-split line #\:)) (string-split-newlines s)))) (define (secinfo name) (assoc "sec" (gpg-with-colons `(--list-secret-key ,name)))) (define (ssbinfo name) (assoc "ssb" (gpg-with-colons `(--list-secret-key ,name)))) (define (fingerprint name) (:fpr (assoc "fpr" (gpg-with-colons `(--list-secret-key ,name))))) ;; convenient accessors for sec (define (:cardinfo x) (list-ref x 14)) ;; Convenient accessors for the colon output of pub. (define (:type x) (string->symbol (list-ref x 0))) (define (:length x) (string->number (list-ref x 2))) (define (:alg x) (string->number (list-ref x 3))) (define (:expire x) (list-ref x 6)) (define (:fpr x) (list-ref x 9)) (define (:cap x) (list-ref x 11)) (define (have-public-key? key) (catch #f (pair? (filter (lambda (l) (and (equal? 'fpr (:type l)) (equal? key::fpr (:fpr l)))) (gpg-with-colons `(--list-keys ,key::fpr)))))) (define (have-secret-key? key) (catch #f (pair? (filter (lambda (l) (and (equal? 'fpr (:type l)) (equal? key::fpr (:fpr l)))) (gpg-with-colons `(--list-secret-keys ,key::fpr)))))) (define (have-secret-key-file? key) (file-exists? (path-join (getenv "GNUPGHOME") "private-keys-v1.d" (string-append key::grip ".key")))) (define (get-config what) (string-split (caddar (gpg-with-colons `(--list-config ,what))) #\;)) (define all-pubkey-algos (delay (get-config "pubkeyname"))) (define all-hash-algos (delay (get-config "digestname"))) (define all-cipher-algos (delay (get-config "ciphername"))) (define all-compression-algos (delay (get-config "compressname"))) (define (have-pubkey-algo? x) (not (not (member x (force all-pubkey-algos))))) (define (have-hash-algo? x) (not (not (member x (force all-hash-algos))))) (define (have-cipher-algo? x) (not (not (member x (force all-cipher-algos))))) (define (have-compression-algo? x) (not (not (member x (force all-compression-algos))))) (define (gpg-pipe args0 args1 errfd) (lambda (source sink) (let* ((p (pipe)) - (task0 (spawn-process-fd `(,@GPG ,@args0) + (task0 (process-spawn-fd `(,@GPG ,@args0) source (:write-end p) errfd)) (_ (close (:write-end p))) - (task1 (spawn-process-fd `(,@GPG ,@args1) + (task1 (process-spawn-fd `(,@GPG ,@args1) (:read-end p) sink errfd))) (close (:read-end p)) - (wait-processes (list GPG GPG) (list task0 task1) #t)))) + (process-wait task0 #t) + (process-wait task1 #t)))) ;; ;; Do we have a software tpm ;; (define have-swtpm? (not (and (string=? "" (getenv "TPMSERVER")) (string=? "" (getenv "SWTPM"))))) (setenv "GPG_AGENT_INFO" "" #t) (setenv "GNUPGHOME" (getcwd) #t) (if have-swtpm? (setenv "TPM_INTERFACE_TYPE" "socsim" #t)) (define GNUPGHOME (getcwd)) ;; ;; GnuPG helper. ;; ;; Call GPG to obtain the hash sums. Either specify an input file in ;; ARGS, or an string in INPUT. Returns a list of ( ;; "") lists. (define (gpg-hash-string args input) (map (lambda (line) (let ((p (string-split line #\:))) (list (string->number (cadr p)) (caddr p)))) (string-split-newlines (call-popen `(,@GPG --with-colons ,@args) input)))) ;; Dearmor a file. (define (dearmor source-name sink-name) (pipe:do (pipe:open source-name (logior O_RDONLY O_BINARY)) (pipe:spawn `(,@GPG --dearmor)) (pipe:write-to sink-name (logior O_WRONLY O_CREAT O_BINARY) #o600))) (define (gpg-dump-packets source-name sink-name) (pipe:do (pipe:open source-name (logior O_RDONLY O_BINARY)) (pipe:spawn `(,@GPG --list-packets)) (pipe:write-to sink-name (logior O_WRONLY O_CREAT O_BINARY) #o600))) ;; ;; Support for test environment creation and teardown. ;; (define (make-test-data filename size) (call-with-binary-output-file filename (lambda (port) (display (make-random-string size) port)))) (define (create-file name . lines) (catch #f (unlink name)) (letfd ((fd (open name (logior O_WRONLY O_CREAT O_BINARY) #o600))) (let ((port (fdopen fd "wb"))) (for-each (lambda (line) (display line port) (newline port)) lines)))) (define (create-gpghome) (log "Creating test environment...") (srandom (getpid)) (make-test-data "random_seed" 600) (log "Creating configuration files") (if (flag "--use-keyring" *args*) (create-file "pubring.gpg")) (create-file "gpg.conf" ;;"log-file socket:///tmp/S.wklog" ;;"verbose" "no-greeting" "no-secmem-warning" "no-permission-warning" "batch" "no-auto-key-retrieve" "no-auto-key-locate" "allow-weak-digest-algos" "ignore-mdc-error" (if have-opt-always-trust "no-auto-check-trustdb" "#no-auto-check-trustdb") (string-append "agent-program " (tool 'gpg-agent) "|--debug-quick-random\n") (if (flag "--use-keyboxd" *args*) "use-keyboxd" "#use-keyboxd") ) (create-file "gpg-agent.conf" "allow-preset-passphrase" "debug-all" "log-file gpg-agent.log" "no-grab" "enable-ssh-support" "s2k-count 65536" (string-append "pinentry-program " (tool 'pinentry)) (string-append "tpm2daemon-program " (tool 'tpm2daemon)) "disable-scdaemon") (create-file "msg.txt" "This is a test of TPM signing and encryption" "With two lines of text")) ;; Initialize the test environment, install appropriate configuration ;; and start the agent, without any keys. (define (setup-environment) (create-gpghome) (start-agent) (start-tpm)) (define (setup-environment-no-atexit) (create-gpghome) (start-agent #t)) ;; Initialize the test environment, install appropriate configuration ;; and start the agent, with the keys from the legacy test suite. (define (setup-legacy-environment) (create-gpghome) (if (member "--unpack-tarball" *args*) (begin (call-check `(,(tool 'gpgtar) --extract --directory=. ,(cadr *args*))) (start-agent)) (begin (start-agent) (create-legacy-gpghome))) (preset-passphrases)) ;; start the tpm server (define (start-tpm) (if have-swtpm? (begin (define pid (call-check `(,(in-srcdir "tests" "tpm2dtests" "start_sw_tpm.sh")))) (if (not (null? pid)) (atexit (lambda () (call-check `("/bin/kill" ,pid)))))))) ;; Create the socket dir and start the agent. (define (start-agent . args) (log "Starting gpg-agent...") (let ((gnupghome (getenv "GNUPGHOME"))) (if (null? args) (atexit (lambda () (with-home-directory gnupghome (stop-agent)))))) (catch (log "Warning: Creating socket directory failed:" (car *error*)) (gpg-conf '--create-socketdir)) (call-check `(,(tool 'gpg-connect-agent) --verbose ,(string-append "--agent-program=" (tool 'gpg-agent) "|--debug-quick-random") /bye))) ;; Stop the agent and other daemons and remove the socket dir. (define (stop-agent) (log "Stopping gpg-agent...") (gpg-conf '--kill 'all) (catch (log "Warning: Removing socket directory failed.") (gpg-conf '--remove-socketdir))) ;; Get the trust level for KEYID. Any remaining arguments are simply ;; passed to GPG. ;; ;; This function only supports keys with a single user id. (define (gettrust keyid . args) (let ((trust (list-ref (assoc "pub" (gpg-with-colons `(,@args --list-keys ,keyid))) 1))) (unless (and (= 1 (string-length trust)) (member (string-ref trust 0) (string->list "oidreqnmfuws-"))) (fail "Bad trust value:" trust)) trust)) ;; Check that KEYID's trust level matches EXPECTED-TRUST. Any ;; remaining arguments are simply passed to GPG. ;; ;; This function only supports keys with a single user id. (define (checktrust keyid expected-trust . args) (let ((trust (apply gettrust `(,keyid ,@args)))) (unless (string=? trust expected-trust) (fail keyid ": Expected trust to be" expected-trust "but got" trust)))) (define (keytotpm name select) (let ((result (call-with-io `(,@GPG --command-fd=0 --edit-key ,name ,select keytotpm) "y\n"))) (if (= 0 (:retcode result)) (:stdout result) (throw "keytotpm failed" (:stderr result))))) (define (quick-gen name algo) (info "creating TPM " algo " key") (call-check `(,@GPG --quick-generate-key ,name ,algo)) (keytotpm name "key 0") (unless (string=? (:cardinfo (secinfo name)) "TPM-Protected") (throw "key is not in the TPM"))) (define (quick-add name algo) (info "adding TPM encryption " algo " key") (call-check `(,@GPG --quick-add-key ,(fingerprint name) ,algo "encr")) (keytotpm name "key 1") (unless (string=? (:cardinfo (ssbinfo name)) "TPM-Protected") (throw "Added key is not in the TPM"))) (define (check-sig name) (info "checking TPM signing") (call-check `(,@GPG --default-key ,name --sign msg.txt)) (call-check `(,@GPG --verify msg.txt.gpg)) (unlink "msg.txt.gpg")) (define (check-encrypt name) (info "Checking TPM decryption") (call-check `(,@GPG --recipient ,name --encrypt msg.txt)) (call-check `(,@GPG --output msg.out.txt --decrypt msg.txt.gpg)) (unless (file=? "msg.txt" "msg.out.txt") (throw "File did not decrypt to the same message")) (unlink "msg.out.txt") (unlink "msg.txt.gpg")) ;; ;; Tests are very simple: create primary key in TPM add encryption key ;; in TPM (verifies TPM primary can certify secondary), sign a message ;; with primary key and check signature encrypt a message with ;; encryption key and check signature ;; (define (test-tpm name algo) (quick-gen name algo) (quick-add name algo) (check-sig name) (check-encrypt name)) ;; ;; Enable checking with valgrind if the envvar "with_valgrind" is set ;; (when with-valgrind? (set! gpg `(,@valgrind ,@gpg))) ;;(set! *args* (append *args* (list "--use-keyboxd"))) ;; end diff --git a/tools/gpg-card.c b/tools/gpg-card.c index a94aabea9..919e61195 100644 --- a/tools/gpg-card.c +++ b/tools/gpg-card.c @@ -1,4254 +1,4254 @@ /* gpg-card.c - An interactive tool to work with cards. * Copyright (C) 2019--2022 g10 Code GmbH * * This file is part of GnuPG. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include #include #include #ifdef HAVE_LIBREADLINE # define GNUPG_LIBREADLINE_H_INCLUDED # include #endif /*HAVE_LIBREADLINE*/ #define INCLUDED_BY_MAIN_MODULE 1 #include "../common/util.h" #include "../common/status.h" #include "../common/i18n.h" #include "../common/init.h" #include "../common/sysutils.h" #include "../common/asshelp.h" #include "../common/userids.h" #include "../common/ccparray.h" #include "../common/exectool.h" #include "../common/exechelp.h" #include "../common/ttyio.h" #include "../common/server-help.h" #include "../common/openpgpdefs.h" #include "../common/tlv.h" #include "../common/comopt.h" #include "gpg-card.h" #define CONTROL_D ('D' - 'A' + 1) #define HISTORYNAME ".gpg-card_history" /* Constants to identify the commands and options. */ enum opt_values { aNull = 0, oQuiet = 'q', oVerbose = 'v', oDebug = 500, oGpgProgram, oGpgsmProgram, oStatusFD, oWithColons, oNoAutostart, oAgentProgram, oDisplay, oTTYname, oTTYtype, oXauthority, oLCctype, oLCmessages, oNoKeyLookup, oNoHistory, oChUid, oDummy }; /* The list of commands and options. */ static gpgrt_opt_t opts[] = { ARGPARSE_group (301, ("@\nOptions:\n ")), ARGPARSE_s_n (oVerbose, "verbose", ("verbose")), ARGPARSE_s_n (oQuiet, "quiet", ("be somewhat more quiet")), ARGPARSE_s_s (oDebug, "debug", "@"), ARGPARSE_s_s (oGpgProgram, "gpg", "@"), ARGPARSE_s_s (oGpgsmProgram, "gpgsm", "@"), ARGPARSE_s_i (oStatusFD, "status-fd", N_("|FD|write status info to this FD")), ARGPARSE_s_n (oWithColons, "with-colons", "@"), ARGPARSE_s_n (oNoAutostart, "no-autostart", "@"), ARGPARSE_s_s (oAgentProgram, "agent-program", "@"), ARGPARSE_s_s (oDisplay, "display", "@"), ARGPARSE_s_s (oTTYname, "ttyname", "@"), ARGPARSE_s_s (oTTYtype, "ttytype", "@"), ARGPARSE_s_s (oXauthority, "xauthority", "@"), ARGPARSE_s_s (oLCctype, "lc-ctype", "@"), ARGPARSE_s_s (oLCmessages, "lc-messages","@"), ARGPARSE_s_n (oNoKeyLookup,"no-key-lookup", "use --no-key-lookup for \"list\""), ARGPARSE_s_n (oNoHistory,"no-history", "do not use the command history file"), ARGPARSE_s_s (oChUid, "chuid", "@"), ARGPARSE_end () }; /* The list of supported debug flags. */ static struct debug_flags_s debug_flags [] = { { DBG_IPC_VALUE , "ipc" }, { DBG_EXTPROG_VALUE, "extprog" }, { 0, NULL } }; /* An object to create lists of labels and keyrefs. */ struct keyinfolabel_s { const char *label; const char *keyref; }; typedef struct keyinfolabel_s *keyinfolabel_t; /* Helper for --chuid. */ static const char *changeuser; /* Limit of size of data we read from a file for certain commands. */ #define MAX_GET_DATA_FROM_FILE 16384 /* Constants for OpenPGP cards. */ #define OPENPGP_USER_PIN_DEFAULT "123456" #define OPENPGP_ADMIN_PIN_DEFAULT "12345678" #define OPENPGP_KDF_DATA_LENGTH_MIN 90 #define OPENPGP_KDF_DATA_LENGTH_MAX 110 /* Local prototypes. */ static void show_keysize_warning (void); static gpg_error_t dispatch_command (card_info_t info, const char *command); static void interactive_loop (void); #ifdef HAVE_LIBREADLINE static char **command_completion (const char *text, int start, int end); #endif /*HAVE_LIBREADLINE*/ /* Print usage information and provide strings for help. */ static const char * my_strusage( int level ) { const char *p; switch (level) { case 9: p = "GPL-3.0-or-later"; break; case 11: p = "gpg-card"; break; case 12: p = "@GNUPG@"; break; case 13: p = VERSION; break; case 14: p = GNUPG_DEF_COPYRIGHT_LINE; break; case 17: p = PRINTABLE_OS_NAME; break; case 19: p = ("Please report bugs to <@EMAIL@>.\n"); break; case 1: case 40: p = ("Usage: gpg-card" " [options] [{[--] command [args]}] (-h for help)"); break; case 41: p = ("Syntax: gpg-card" " [options] [command [args] {-- command [args]}]\n\n" "Tool to manage cards and tokens. Without a command an interactive\n" "mode is used. Use command \"help\" to list all commands."); break; default: p = NULL; break; } return p; } static void set_opt_session_env (const char *name, const char *value) { gpg_error_t err; err = session_env_setenv (opt.session_env, name, value); if (err) log_fatal ("error setting session environment: %s\n", gpg_strerror (err)); } /* Command line parsing. */ static void parse_arguments (gpgrt_argparse_t *pargs, gpgrt_opt_t *popts) { while (gpgrt_argparse (NULL, pargs, popts)) { switch (pargs->r_opt) { case oQuiet: opt.quiet = 1; break; case oVerbose: opt.verbose++; break; case oDebug: if (parse_debug_flag (pargs->r.ret_str, &opt.debug, debug_flags)) { pargs->r_opt = ARGPARSE_INVALID_ARG; pargs->err = ARGPARSE_PRINT_ERROR; } break; case oGpgProgram: opt.gpg_program = pargs->r.ret_str; break; case oGpgsmProgram: opt.gpgsm_program = pargs->r.ret_str; break; case oAgentProgram: opt.agent_program = pargs->r.ret_str; break; case oStatusFD: gnupg_set_status_fd (translate_sys2libc_fd_int (pargs->r.ret_int, 1)); break; case oWithColons: opt.with_colons = 1; break; case oNoAutostart: opt.autostart = 0; break; case oDisplay: set_opt_session_env ("DISPLAY", pargs->r.ret_str); break; case oTTYname: set_opt_session_env ("GPG_TTY", pargs->r.ret_str); break; case oTTYtype: set_opt_session_env ("TERM", pargs->r.ret_str); break; case oXauthority: set_opt_session_env ("XAUTHORITY", pargs->r.ret_str); break; case oLCctype: opt.lc_ctype = pargs->r.ret_str; break; case oLCmessages: opt.lc_messages = pargs->r.ret_str; break; case oNoKeyLookup: opt.no_key_lookup = 1; break; case oNoHistory: opt.no_history = 1; break; case oChUid: changeuser = pargs->r.ret_str; break; default: pargs->err = 2; break; } } } /* gpg-card main. */ int main (int argc, char **argv) { gpg_error_t err; gpgrt_argparse_t pargs; char **command_list = NULL; int cmdidx; char *command; gnupg_reopen_std ("gpg-card"); gpgrt_set_strusage (my_strusage); gnupg_rl_initialize (); log_set_prefix ("gpg-card", GPGRT_LOG_WITH_PREFIX); /* Make sure that our subsystems are ready. */ i18n_init(); init_common_subsystems (&argc, &argv); assuan_set_gpg_err_source (GPG_ERR_SOURCE_DEFAULT); setup_libassuan_logging (&opt.debug, NULL); /* Setup default options. */ opt.autostart = 1; opt.session_env = session_env_new (); if (!opt.session_env) log_fatal ("error allocating session environment block: %s\n", gpg_strerror (gpg_error_from_syserror ())); /* Parse the command line. */ pargs.argc = &argc; pargs.argv = &argv; pargs.flags = ARGPARSE_FLAG_KEEP; parse_arguments (&pargs, opts); gpgrt_argparse (NULL, &pargs, NULL); /* Release internal state. */ if (changeuser && gnupg_chuid (changeuser, 0)) log_inc_errorcount (); /* Force later termination. */ if (log_get_errorcount (0)) exit (2); /* Process common component options. */ gpgrt_set_confdir (GPGRT_CONFDIR_SYS, gnupg_sysconfdir ()); gpgrt_set_confdir (GPGRT_CONFDIR_USER, gnupg_homedir ()); if (parse_comopt (GNUPG_MODULE_NAME_CARD, opt.debug)) { gnupg_status_printf (STATUS_FAILURE, "option-parser %u", gpg_error (GPG_ERR_GENERAL)); exit(2); } if (comopt.no_autostart) opt.autostart = 0; /* Set defaults for non given options. */ if (!opt.gpg_program) opt.gpg_program = gnupg_module_name (GNUPG_MODULE_NAME_GPG); if (!opt.gpgsm_program) opt.gpgsm_program = gnupg_module_name (GNUPG_MODULE_NAME_GPGSM); /* Now build the list of commands. We guess the size of the array * by assuming each item is a complete command. Obviously this will * be rarely the case, but it is less code to allocate a possible * too large array. */ command_list = xcalloc (argc+1, sizeof *command_list); cmdidx = 0; command = NULL; while (argc) { for ( ; argc && strcmp (*argv, "--"); argc--, argv++) { if (!command) command = xstrdup (*argv); else { char *tmp = xstrconcat (command, " ", *argv, NULL); xfree (command); command = tmp; } } if (argc) { /* Skip the double dash. */ argc--; argv++; } if (command) { command_list[cmdidx++] = command; command = NULL; } } opt.interactive = !cmdidx; if (!opt.interactive) opt.no_history = 1; if (opt.interactive) { interactive_loop (); err = 0; } else { struct card_info_s info_buffer = { 0 }; card_info_t info = &info_buffer; err = 0; for (cmdidx=0; (command = command_list[cmdidx]); cmdidx++) { err = dispatch_command (info, command); if (err) break; } if (gpg_err_code (err) == GPG_ERR_EOF) err = 0; /* This was a "quit". */ else if (command && !opt.quiet) log_info ("stopped at command '%s'\n", command); } flush_keyblock_cache (); if (command_list) { for (cmdidx=0; command_list[cmdidx]; cmdidx++) xfree (command_list[cmdidx]); xfree (command_list); } if (err) gnupg_status_printf (STATUS_FAILURE, "- %u", err); else if (log_get_errorcount (0)) gnupg_status_printf (STATUS_FAILURE, "- %u", GPG_ERR_GENERAL); else gnupg_status_printf (STATUS_SUCCESS, NULL); return log_get_errorcount (0)? 1:0; } /* Return S or the string "[none]" if S is NULL. */ static GPGRT_INLINE const char * nullnone (const char *s) { return s? s: "[none]"; } /* Read data from file FNAME up to MAX_GET_DATA_FROM_FILE characters. * On error return an error code and stores NULL at R_BUFFER; on * success returns 0 and stores the number of bytes read at R_BUFLEN * and the address of a newly allocated buffer at R_BUFFER. A * complementary nul byte is always appended to the data but not * counted; this allows to pass NULL for R-BUFFER and consider the * returned data as a string. */ static gpg_error_t get_data_from_file (const char *fname, char **r_buffer, size_t *r_buflen) { gpg_error_t err; estream_t fp; char *data; int n; *r_buffer = NULL; if (r_buflen) *r_buflen = 0; fp = es_fopen (fname, "rb"); if (!fp) { err = gpg_error_from_syserror (); log_error (_("can't open '%s': %s\n"), fname, gpg_strerror (err)); return err; } data = xtrymalloc (MAX_GET_DATA_FROM_FILE); if (!data) { err = gpg_error_from_syserror (); log_error (_("error allocating enough memory: %s\n"), gpg_strerror (err)); es_fclose (fp); return err; } n = es_fread (data, 1, MAX_GET_DATA_FROM_FILE - 1, fp); es_fclose (fp); if (n < 0) { err = gpg_error_from_syserror (); tty_printf (_("error reading '%s': %s\n"), fname, gpg_strerror (err)); xfree (data); return err; } data[n] = 0; *r_buffer = data; if (r_buflen) *r_buflen = n; return 0; } /* Fixup the ENODEV error from scdaemon which we may see after * removing a card due to scdaemon scanning for readers with cards. * We also map the CAERD REMOVED error to the more useful CARD_NOT * PRESENT. */ static gpg_error_t fixup_scd_errors (gpg_error_t err) { if ((gpg_err_code (err) == GPG_ERR_ENODEV || gpg_err_code (err) == GPG_ERR_CARD_REMOVED) && gpg_err_source (err) == GPG_ERR_SOURCE_SCD) err = gpg_error (GPG_ERR_CARD_NOT_PRESENT); return err; } /* Set the card removed flag from INFO depending on ERR. This does * not clear the flag. */ static gpg_error_t maybe_set_card_removed (card_info_t info, gpg_error_t err) { if ((gpg_err_code (err) == GPG_ERR_ENODEV || gpg_err_code (err) == GPG_ERR_CARD_REMOVED) && gpg_err_source (err) == GPG_ERR_SOURCE_SCD) info->card_removed = 1; return err; } /* Write LENGTH bytes from BUFFER to file FNAME. Return 0 on * success. */ static gpg_error_t put_data_to_file (const char *fname, const void *buffer, size_t length) { gpg_error_t err; estream_t fp; fp = es_fopen (fname, "wb"); if (!fp) { err = gpg_error_from_syserror (); log_error (_("can't create '%s': %s\n"), fname, gpg_strerror (err)); return err; } if (length && es_fwrite (buffer, length, 1, fp) != 1) { err = gpg_error_from_syserror (); log_error (_("error writing '%s': %s\n"), fname, gpg_strerror (err)); es_fclose (fp); return err; } if (es_fclose (fp)) { err = gpg_error_from_syserror (); log_error (_("error writing '%s': %s\n"), fname, gpg_strerror (err)); return err; } return 0; } /* Return a malloced string with the number opf the menu PROMPT. * Control-D is mapped to "Q". */ static char * get_selection (const char *prompt) { char *answer; tty_printf ("\n"); tty_printf ("%s", prompt); tty_printf ("\n"); answer = tty_get (_("Your selection? ")); tty_kill_prompt (); if (*answer == CONTROL_D) strcpy (answer, "q"); return answer; } /* Simply prints TEXT to the output. Returns 0 as a convenience. * This is a separate function so that it can be extended to run * less(1) or so. The extra arguments are int values terminated by a * 0 to indicate card application types supported with this command. * If none are given (just the final 0), this is a general * command. */ static gpg_error_t print_help (const char *text, ...) { estream_t fp; va_list arg_ptr; int value; int any = 0; fp = opt.interactive? NULL : es_stdout; tty_fprintf (fp, "%s\n", text); va_start (arg_ptr, text); while ((value = va_arg (arg_ptr, int))) { if (!any) tty_fprintf (fp, "[Supported by: "); tty_fprintf (fp, "%s%s", any?", ":"", app_type_string (value)); any = 1; } if (any) tty_fprintf (fp, "]\n"); va_end (arg_ptr); return 0; } /* Print an (OpenPGP) fingerprint. */ static void print_shax_fpr (estream_t fp, const unsigned char *fpr, unsigned int fprlen) { int i; if (fpr) { for (i=0; i < fprlen ; i++, fpr++) tty_fprintf (fp, "%02X", *fpr); } else tty_fprintf (fp, " [none]"); tty_fprintf (fp, "\n"); } /* Print the keygrip GRP. */ static void print_keygrip (estream_t fp, const unsigned char *grp) { int i; for (i=0; i < 20 ; i++, grp++) tty_fprintf (fp, "%02X", *grp); tty_fprintf (fp, "\n"); } /* Print a string but avoid printing control characters. */ static void print_string (estream_t fp, const char *text, const char *name) { tty_fprintf (fp, "%s", text); /* FIXME: tty_printf_utf8_string2 eats everything after and including an @ - e.g. when printing an url. */ if (name && *name) { if (fp) print_utf8_buffer2 (fp, name, strlen (name), '\n'); else tty_print_utf8_string2 (NULL, name, strlen (name), 0); } else tty_fprintf (fp, _("[not set]")); tty_fprintf (fp, "\n"); } /* Print an ISO formatted name or "[not set]". */ static void print_isoname (estream_t fp, const char *name) { if (name && *name) { char *p, *given, *buf; buf = xstrdup (name); given = strstr (buf, "<<"); for (p=buf; *p; p++) if (*p == '<') *p = ' '; if (given && given[2]) { *given = 0; given += 2; if (fp) print_utf8_buffer2 (fp, given, strlen (given), '\n'); else tty_print_utf8_string2 (NULL, given, strlen (given), 0); if (*buf) tty_fprintf (fp, " "); } if (fp) print_utf8_buffer2 (fp, buf, strlen (buf), '\n'); else tty_print_utf8_string2 (NULL, buf, strlen (buf), 0); xfree (buf); } else { tty_fprintf (fp, _("[not set]")); } tty_fprintf (fp, "\n"); } /* Return true if the buffer MEM of length memlen consists only of zeroes. */ static int mem_is_zero (const char *mem, unsigned int memlen) { int i; for (i=0; i < memlen && !mem[i]; i++) ; return (i == memlen); } /* Helper to list a single keyref. LABEL_KEYREF is a fallback key * reference if no info is available; it may be NULL. */ static void list_one_kinfo (card_info_t info, key_info_t kinfo, const char *label_keyref, estream_t fp, int no_key_lookup, int create_shadow) { gpg_error_t err; key_info_t firstkinfo = info->kinfo; keyblock_t keyblock = NULL; keyblock_t kb; pubkey_t pubkey; userid_t uid; key_info_t ki; const char *s; gcry_sexp_t s_pkey; int any; if (firstkinfo && kinfo) { tty_fprintf (fp, " "); if (mem_is_zero (kinfo->grip, sizeof kinfo->grip)) { tty_fprintf (fp, "[none]\n"); tty_fprintf (fp, " keyref .....: %s\n", kinfo->keyref); if (kinfo->label) tty_fprintf (fp, " label ......: %s\n", kinfo->label); tty_fprintf (fp, " algorithm ..: %s\n", nullnone (kinfo->keyalgo)); goto leave; } print_keygrip (fp, kinfo->grip); tty_fprintf (fp, " keyref .....: %s", kinfo->keyref); if (kinfo->usage) { any = 0; tty_fprintf (fp, " ("); if ((kinfo->usage & GCRY_PK_USAGE_SIGN)) { tty_fprintf (fp, "sign"); any=1; } if ((kinfo->usage & GCRY_PK_USAGE_CERT)) { tty_fprintf (fp, "%scert", any?",":""); any=1; } if ((kinfo->usage & GCRY_PK_USAGE_AUTH)) { tty_fprintf (fp, "%sauth", any?",":""); any=1; } if ((kinfo->usage & GCRY_PK_USAGE_ENCR)) { tty_fprintf (fp, "%sencr", any?",":""); any=1; } tty_fprintf (fp, ")"); } tty_fprintf (fp, "\n"); if (kinfo->label) tty_fprintf (fp, " label ......: %s\n", kinfo->label); if (!(err = scd_readkey (kinfo->keyref, create_shadow, &s_pkey))) { char *tmp = pubkey_algo_string (s_pkey, NULL); tty_fprintf (fp, " algorithm ..: %s\n", nullnone (tmp)); xfree (tmp); gcry_sexp_release (s_pkey); s_pkey = NULL; } else { maybe_set_card_removed (info, err); tty_fprintf (fp, " algorithm ..: %s\n", nullnone (kinfo->keyalgo)); } if (kinfo->fprlen && kinfo->created) { tty_fprintf (fp, " stored fpr .: "); print_shax_fpr (fp, kinfo->fpr, kinfo->fprlen); tty_fprintf (fp, " created ....: %s\n", isotimestamp (kinfo->created)); } if (no_key_lookup) err = 0; else err = get_matching_keys (kinfo->grip, (GNUPG_PROTOCOL_OPENPGP | GNUPG_PROTOCOL_CMS), &keyblock); if (err) { if (gpg_err_code (err) != GPG_ERR_NO_PUBKEY) tty_fprintf (fp, " used for ...: [%s]\n", gpg_strerror (err)); goto leave; } for (kb = keyblock; kb; kb = kb->next) { tty_fprintf (fp, " used for ...: %s\n", kb->protocol == GNUPG_PROTOCOL_OPENPGP? "OpenPGP" : kb->protocol == GNUPG_PROTOCOL_CMS? "X.509" : "?"); pubkey = kb->keys; if (kb->protocol == GNUPG_PROTOCOL_OPENPGP) { /* If this is not the primary key print the primary * key's fingerprint or a reference to it. */ tty_fprintf (fp, " main key .: "); for (ki=firstkinfo; ki; ki = ki->next) if (pubkey->grip_valid && !memcmp (ki->grip, pubkey->grip, KEYGRIP_LEN)) break; if (ki) { /* Fixme: Replace mapping by a table lookup. */ if (!memcmp (kinfo->grip, pubkey->grip, KEYGRIP_LEN)) s = "this"; else if (!strcmp (ki->keyref, "OPENPGP.1")) s = "Signature key"; else if (!strcmp (ki->keyref, "OPENPGP.2")) s = "Encryption key"; else if (!strcmp (ki->keyref, "OPENPGP.3")) s = "Authentication key"; else s = NULL; if (s) tty_fprintf (fp, "<%s>\n", s); else tty_fprintf (fp, "\n", ki->keyref); } else /* Print the primary key as fallback. */ print_shax_fpr (fp, pubkey->fpr, pubkey->fprlen); } if (kb->protocol == GNUPG_PROTOCOL_OPENPGP || kb->protocol == GNUPG_PROTOCOL_CMS) { /* Find the primary or subkey of that key. */ for (; pubkey; pubkey = pubkey->next) if (pubkey->grip_valid && !memcmp (kinfo->grip, pubkey->grip, KEYGRIP_LEN)) break; if (pubkey) { tty_fprintf (fp, " fpr ......: "); print_shax_fpr (fp, pubkey->fpr, pubkey->fprlen); tty_fprintf (fp, " created ..: %s\n", isotimestamp (pubkey->created)); } } for (uid = kb->uids; uid; uid = uid->next) { print_string (fp, " user id ..: ", uid->value); } } } else { tty_fprintf (fp, " [none]\n"); if (label_keyref) tty_fprintf (fp, " keyref .....: %s\n", label_keyref); if (kinfo) tty_fprintf (fp, " algorithm ..: %s\n", nullnone (kinfo->keyalgo)); } leave: release_keyblock (keyblock); } /* List all keyinfo in INFO using the list of LABELS. */ static void list_all_kinfo (card_info_t info, keyinfolabel_t labels, estream_t fp, int no_key_lookup, int create_shadow) { key_info_t kinfo; int idx, i, j; /* Print the keyinfo. We first print those we known and then all * remaining item. */ for (kinfo = info->kinfo; kinfo; kinfo = kinfo->next) kinfo->xflag = 0; if (labels) { for (idx=0; labels[idx].label; idx++) { tty_fprintf (fp, "%s", labels[idx].label); kinfo = find_kinfo (info, labels[idx].keyref); list_one_kinfo (info, kinfo, labels[idx].keyref, fp, no_key_lookup, create_shadow); if (kinfo) kinfo->xflag = 1; } } for (kinfo = info->kinfo; kinfo; kinfo = kinfo->next) { if (kinfo->xflag) continue; tty_fprintf (fp, "Key %s", kinfo->keyref); for (i=4+strlen (kinfo->keyref), j=0; i < 18; i++, j=1) tty_fprintf (fp, j? ".":" "); tty_fprintf (fp, ":"); list_one_kinfo (info, kinfo, NULL, fp, no_key_lookup, create_shadow); } } static void list_retry_counter (card_info_t info, estream_t fp) { const char *s; int i; if (info->chvlabels) tty_fprintf (fp, "PIN labels .......: %s\n", info->chvlabels); tty_fprintf (fp, "PIN retry counter :"); for (i=0; i < DIM (info->chvinfo) && i < info->nchvinfo; i++) { if (info->chvinfo[i] >= 0) tty_fprintf (fp, " %d", info->chvinfo[i]); else { switch (info->chvinfo[i]) { case -1: s = "[error]"; break; case -2: s = "-"; break; /* No such PIN or info not available. */ case -3: s = "[blocked]"; break; case -4: s = "[nullpin]"; break; case -5: s = "[verified]"; break; default: s = "[?]"; break; } tty_fprintf (fp, " %s", s); } } tty_fprintf (fp, "\n"); } /* List OpenPGP card specific data. */ static void list_openpgp (card_info_t info, estream_t fp, int no_key_lookup, int create_shadow) { static struct keyinfolabel_s keyinfolabels[] = { { "Signature key ....:", "OPENPGP.1" }, { "Encryption key....:", "OPENPGP.2" }, { "Authentication key:", "OPENPGP.3" }, { NULL, NULL } }; if (info->apptype != APP_TYPE_OPENPGP) { tty_fprintf (fp, "invalid OpenPGP card\n"); return; } tty_fprintf (fp, "Name of cardholder: "); print_isoname (fp, info->disp_name); print_string (fp, "Language prefs ...: ", info->disp_lang); tty_fprintf (fp, "Salutation .......: %s\n", info->disp_sex == 1? _("Mr."): info->disp_sex == 2? _("Ms.") : ""); print_string (fp, "URL of public key : ", info->pubkey_url); print_string (fp, "Login data .......: ", info->login_data); if (info->private_do[0]) print_string (fp, "Private DO 1 .....: ", info->private_do[0]); if (info->private_do[1]) print_string (fp, "Private DO 2 .....: ", info->private_do[1]); if (info->private_do[2]) print_string (fp, "Private DO 3 .....: ", info->private_do[2]); if (info->private_do[3]) print_string (fp, "Private DO 4 .....: ", info->private_do[3]); if (info->cafpr1len) { tty_fprintf (fp, "CA fingerprint %d .:", 1); print_shax_fpr (fp, info->cafpr1, info->cafpr1len); } if (info->cafpr2len) { tty_fprintf (fp, "CA fingerprint %d .:", 2); print_shax_fpr (fp, info->cafpr2, info->cafpr2len); } if (info->cafpr3len) { tty_fprintf (fp, "CA fingerprint %d .:", 3); print_shax_fpr (fp, info->cafpr3, info->cafpr3len); } tty_fprintf (fp, "Signature PIN ....: %s\n", info->chv1_cached? _("not forced"): _("forced")); tty_fprintf (fp, "Max. PIN lengths .: %d %d %d\n", info->chvmaxlen[0], info->chvmaxlen[1], info->chvmaxlen[2]); list_retry_counter (info, fp); tty_fprintf (fp, "Signature counter : %lu\n", info->sig_counter); tty_fprintf (fp, "Capabilities .....:"); if (info->extcap.ki) tty_fprintf (fp, " key-import"); if (info->extcap.aac) tty_fprintf (fp, " algo-change"); if (info->extcap.bt) tty_fprintf (fp, " button"); if (info->extcap.sm) tty_fprintf (fp, " sm(%s)", gcry_cipher_algo_name (info->extcap.smalgo)); if (info->extcap.private_dos) tty_fprintf (fp, " priv-data"); tty_fprintf (fp, "\n"); if (info->extcap.kdf) { tty_fprintf (fp, "KDF setting ......: %s\n", info->kdf_do_enabled ? "on" : "off"); } if (info->extcap.bt) { tty_fprintf (fp, "UIF setting ......: Sign=%s Decrypt=%s Auth=%s\n", info->uif[0] ? (info->uif[0]==2? "permanent": "on") : "off", info->uif[1] ? (info->uif[0]==2? "permanent": "on") : "off", info->uif[2] ? (info->uif[0]==2? "permanent": "on") : "off"); } list_all_kinfo (info, keyinfolabels, fp, no_key_lookup, create_shadow); } /* List PIV card specific data. */ static void list_piv (card_info_t info, estream_t fp, int no_key_lookup, int create_shadow) { static struct keyinfolabel_s keyinfolabels[] = { { "PIV authentication:", "PIV.9A" }, { "Card authenticat. :", "PIV.9E" }, { "Digital signature :", "PIV.9C" }, { "Key management ...:", "PIV.9D" }, { NULL, NULL } }; if (info->chvusage[0] || info->chvusage[1]) { tty_fprintf (fp, "PIN usage policy .:"); if ((info->chvusage[0] & 0x40)) tty_fprintf (fp, " app-pin"); if ((info->chvusage[0] & 0x20)) tty_fprintf (fp, " global-pin"); if ((info->chvusage[0] & 0x10)) tty_fprintf (fp, " occ"); if ((info->chvusage[0] & 0x08)) tty_fprintf (fp, " vci"); if ((info->chvusage[0] & 0x08) && !(info->chvusage[0] & 0x04)) tty_fprintf (fp, " pairing"); if (info->chvusage[1] == 0x10) tty_fprintf (fp, " primary:card"); else if (info->chvusage[1] == 0x20) tty_fprintf (fp, " primary:global"); tty_fprintf (fp, "\n"); } list_retry_counter (info, fp); list_all_kinfo (info, keyinfolabels, fp, no_key_lookup, create_shadow); } /* List Netkey card specific data. */ static void list_nks (card_info_t info, estream_t fp, int no_key_lookup, int create_shadow) { static struct keyinfolabel_s keyinfolabels[] = { { NULL, NULL } }; list_retry_counter (info, fp); list_all_kinfo (info, keyinfolabels, fp, no_key_lookup, create_shadow); } /* List PKCS#15 card specific data. */ static void list_p15 (card_info_t info, estream_t fp, int no_key_lookup, int create_shadow) { static struct keyinfolabel_s keyinfolabels[] = { { NULL, NULL } }; list_retry_counter (info, fp); list_all_kinfo (info, keyinfolabels, fp, no_key_lookup, create_shadow); } static void print_a_version (estream_t fp, const char *prefix, unsigned int value) { unsigned int a, b, c, d; a = ((value >> 24) & 0xff); b = ((value >> 16) & 0xff); c = ((value >> 8) & 0xff); d = ((value ) & 0xff); if (a) tty_fprintf (fp, "%s %u.%u.%u.%u\n", prefix, a, b, c, d); else if (b) tty_fprintf (fp, "%s %u.%u.%u\n", prefix, b, c, d); else if (c) tty_fprintf (fp, "%s %u.%u\n", prefix, c, d); else tty_fprintf (fp, "%s %u\n", prefix, d); } /* Print all available information about the current card. With * NO_KEY_LOOKUP the sometimes expensive listing of all matching * OpenPGP and X.509 keys is not done */ static void list_card (card_info_t info, int no_key_lookup, int create_shadow) { estream_t fp = opt.interactive? NULL : es_stdout; tty_fprintf (fp, "Reader ...........: %s\n", nullnone (info->reader)); if (info->cardtype) tty_fprintf (fp, "Card type ........: %s\n", info->cardtype); if (info->cardversion) print_a_version (fp, "Card firmware ....:", info->cardversion); tty_fprintf (fp, "Serial number ....: %s\n", nullnone (info->serialno)); tty_fprintf (fp, "Application type .: %s%s%s%s\n", app_type_string (info->apptype), info->apptype == APP_TYPE_UNKNOWN && info->apptypestr? "(":"", info->apptype == APP_TYPE_UNKNOWN && info->apptypestr ? info->apptypestr:"", info->apptype == APP_TYPE_UNKNOWN && info->apptypestr? ")":""); if (info->appversion) print_a_version (fp, "Version ..........:", info->appversion); if (info->serialno && info->dispserialno && strcmp (info->serialno, info->dispserialno)) tty_fprintf (fp, "Displayed s/n ....: %s\n", info->dispserialno); if (info->manufacturer_name && info->manufacturer_id) tty_fprintf (fp, "Manufacturer .....: %s (%x)\n", info->manufacturer_name, info->manufacturer_id); else if (info->manufacturer_name && !info->manufacturer_id) tty_fprintf (fp, "Manufacturer .....: %s\n", info->manufacturer_name); else if (info->manufacturer_id) tty_fprintf (fp, "Manufacturer .....: (%x)\n", info->manufacturer_id); switch (info->apptype) { case APP_TYPE_OPENPGP: list_openpgp (info, fp, no_key_lookup, create_shadow); break; case APP_TYPE_PIV: list_piv (info, fp, no_key_lookup, create_shadow); break; case APP_TYPE_NKS: list_nks (info, fp, no_key_lookup, create_shadow); break; case APP_TYPE_P15: list_p15 (info, fp, no_key_lookup, create_shadow); break; default: break; } } /* Helper for cmd_list. */ static void print_card_list (estream_t fp, card_info_t info, strlist_t cards, int only_current) { int count; strlist_t sl; size_t snlen; int star; const char *s; for (count = 0, sl = cards; sl; sl = sl->next, count++) { if (info && info->serialno) { s = strchr (sl->d, ' '); if (s) snlen = s - sl->d; else snlen = strlen (sl->d); star = (strlen (info->serialno) == snlen && !memcmp (info->serialno, sl->d, snlen)); } else star = 0; if (!only_current || star) tty_fprintf (fp, "%d%c %s\n", count, star? '*':' ', sl->d); } } /* The LIST command. This also updates INFO if needed. */ static gpg_error_t cmd_list (card_info_t info, char *argstr) { gpg_error_t err; int opt_cards, opt_apps, opt_info, opt_reread, opt_no_key_lookup; int opt_shadow; strlist_t cards = NULL; strlist_t sl; estream_t fp = opt.interactive? NULL : es_stdout; const char *cardsn = NULL; char *appstr = NULL; int count; int need_learn = 0; if (!info) return print_help ("LIST [--cards] [--apps] [--info] [--reread] [--shadow]" " [--no-key-lookup] [N] [APP]\n\n" "Show the content of the current card.\n" "With N given select and list the N-th card;\n" "with APP also given select that application.\n" "To select an APP on the current card use '-' for N.\n" "The S/N of the card may be used instead of N.\n" " --cards list available cards\n" " --apps list additional card applications\n" " --info select a card and prints its s/n\n" " --reread read infos from PCKS#15 cards again\n" " --shadow create shadow keys for all card keys\n" " --no-key-lookup do not list matching OpenPGP or X.509 keys\n" , 0); opt_cards = has_leading_option (argstr, "--cards"); opt_apps = has_leading_option (argstr, "--apps"); opt_info = has_leading_option (argstr, "--info"); opt_reread = has_leading_option (argstr, "--reread"); opt_shadow = has_leading_option (argstr, "--shadow"); opt_no_key_lookup = has_leading_option (argstr, "--no-key-lookup"); argstr = skip_options (argstr); if (opt_shadow) opt_no_key_lookup = 1; if (opt.no_key_lookup) opt_no_key_lookup = 1; if (hexdigitp (argstr) || (*argstr == '-' && spacep (argstr+1))) { if (*argstr == '-' && (argstr[1] || spacep (argstr+1))) argstr++; /* Keep current card. */ else { cardsn = argstr; while (hexdigitp (argstr)) argstr++; if (*argstr && !spacep (argstr)) { err = gpg_error (GPG_ERR_INV_ARG); goto leave; } if (*argstr) *argstr++ = 0; } while (spacep (argstr)) argstr++; if (*argstr) { appstr = argstr; while (*argstr && !spacep (argstr)) argstr++; while (spacep (argstr)) argstr++; if (*argstr) { /* Extra arguments found. */ err = gpg_error (GPG_ERR_INV_ARG); goto leave; } } } else if (*argstr) { /* First argument needs to be a digit. */ err = gpg_error (GPG_ERR_INV_ARG); goto leave; } if (!info->serialno || info->need_sn_cmd) { /* This is probably the first call or was explicitly requested. * We need to send a SERIALNO command to scdaemon so that our * session knows all cards. */ err = scd_serialno (NULL, NULL); if (err) goto leave; info->need_sn_cmd = 0; need_learn = 1; } if (opt_cards || opt_apps) { /* Note that with option --apps CARDS is here the list of all * apps. Format is "SERIALNO APPNAME {APPNAME}". We print the * card number in the first column. */ if (opt_apps) err = scd_applist (&cards, opt_cards); else err = scd_cardlist (&cards); if (err) goto leave; print_card_list (fp, info, cards, 0); } else { if (cardsn) { int i, cardno; err = scd_cardlist (&cards); if (err) goto leave; /* Switch to the requested card. */ for (i=0; digitp (cardsn+i); i++) ; if (i && i < 4 && !cardsn[i]) { /* Looks like an index into the card list. */ cardno = atoi (cardsn); for (count = 0, sl = cards; sl; sl = sl->next, count++) if (count == cardno) break; if (!sl) { err = gpg_error (GPG_ERR_INV_INDEX); goto leave; } } else /* S/N of card specified. */ { for (sl = cards; sl; sl = sl->next) if (!ascii_strcasecmp (sl->d, cardsn)) break; if (!sl) { err = gpg_error (GPG_ERR_INV_INDEX); goto leave; } } err = scd_switchcard (sl->d); need_learn = 1; } else /* show app list. */ { err = scd_applist (&cards, 1); if (err) goto leave; } if (appstr && *appstr) { /* Switch to the requested app. */ err = scd_switchapp (appstr); if (err) goto leave; need_learn = 1; } if (need_learn) err = scd_learn (info, opt_reread); else err = 0; if (err) ; else if (opt_info) print_card_list (fp, info, cards, 1); else { size_t snlen; const char *s; /* First get the list of active cards and check whether the * current card is still in the list. If not the card has * been removed. Note that during the listing the card * remove state might also be detected but only if an access * to the scdaemon is required; it is anyway better to test * that before starting a listing. */ free_strlist (cards); err = scd_cardlist (&cards); if (err) goto leave; for (sl = cards; sl; sl = sl->next) { if (info && info->serialno) { s = strchr (sl->d, ' '); if (s) snlen = s - sl->d; else snlen = strlen (sl->d); if (strlen (info->serialno) == snlen && !memcmp (info->serialno, sl->d, snlen)) break; } } if (!sl) { info->need_sn_cmd = 1; err = gpg_error (GPG_ERR_CARD_REMOVED); goto leave; } list_card (info, opt_no_key_lookup, opt_shadow); } } leave: free_strlist (cards); return err; } /* The VERIFY command. */ static gpg_error_t cmd_verify (card_info_t info, char *argstr) { gpg_error_t err, err2; const char *pinref; if (!info) return print_help ("verify [chvid]", 0); if (*argstr) pinref = argstr; else if (info->apptype == APP_TYPE_OPENPGP) pinref = info->serialno; else if (info->apptype == APP_TYPE_PIV) pinref = "PIV.80"; else return gpg_error (GPG_ERR_MISSING_VALUE); err = scd_checkpin (pinref); if (err) log_error ("verify failed: %s <%s>\n", gpg_strerror (err), gpg_strsource (err)); /* In any case update the CHV status, so that the next "list" shows * the correct retry counter values. */ err2 = scd_getattr ("CHV-STATUS", info); return err ? err : err2; } static gpg_error_t cmd_authenticate (card_info_t info, char *argstr) { gpg_error_t err; int opt_setkey; int opt_raw; char *string = NULL; char *key = NULL; size_t keylen; if (!info) return print_help ("AUTHENTICATE [--setkey] [--raw] [< FILE]|KEY\n\n" "Perform a mutual authentication either by reading the key\n" "from FILE or by taking it from the command line. Without\n" "the option --raw the key is expected to be hex encoded.\n" "To install a new administration key --setkey is used; this\n" "requires a prior authentication with the old key.", APP_TYPE_PIV, 0); if (info->apptype != APP_TYPE_PIV) { log_info ("Note: This is a PIV only command.\n"); return gpg_error (GPG_ERR_NOT_SUPPORTED); } opt_setkey = has_leading_option (argstr, "--setkey"); opt_raw = has_leading_option (argstr, "--raw"); argstr = skip_options (argstr); if (*argstr == '<') /* Read key from a file. */ { for (argstr++; spacep (argstr); argstr++) ; err = get_data_from_file (argstr, &string, NULL); if (err) goto leave; } if (opt_raw) { key = string? string : xstrdup (argstr); string = NULL; keylen = strlen (key); } else { key = hex_to_buffer (string? string: argstr, &keylen); if (!key) { err = gpg_error_from_syserror (); goto leave; } } err = scd_setattr (opt_setkey? "SET-ADM-KEY":"AUTH-ADM-KEY", key, keylen); leave: if (key) { wipememory (key, keylen); xfree (key); } xfree (string); return err; } /* Helper for cmd_name to qyery a part of name. */ static char * ask_one_name (const char *prompt) { char *name; int i; for (;;) { name = tty_get (prompt); trim_spaces (name); tty_kill_prompt (); if (!*name || *name == CONTROL_D) { if (*name == CONTROL_D) tty_fprintf (NULL, "\n"); xfree (name); return NULL; } for (i=0; name[i] && name[i] >= ' ' && name[i] <= 126; i++) ; /* The name must be in Latin-1 and not UTF-8 - lacking the code * to ensure this we restrict it to ASCII. */ if (name[i]) tty_printf (_("Error: Only plain ASCII is currently allowed.\n")); else if (strchr (name, '<')) tty_printf (_("Error: The \"<\" character may not be used.\n")); else if (strstr (name, " ")) tty_printf (_("Error: Double spaces are not allowed.\n")); else return name; xfree (name); } } /* The NAME command. */ static gpg_error_t cmd_name (card_info_t info, const char *argstr) { gpg_error_t err; char *surname, *givenname; char *isoname, *p; if (!info) return print_help ("name [--clear]\n\n" "Set the name field of an OpenPGP card. With --clear the stored\n" "name is cleared off the card.", APP_TYPE_OPENPGP, APP_TYPE_NKS, 0); if (info->apptype != APP_TYPE_OPENPGP) { log_info ("Note: This is an OpenPGP only command.\n"); return gpg_error (GPG_ERR_NOT_SUPPORTED); } again: if (!strcmp (argstr, "--clear")) isoname = xstrdup (" "); /* No real way to clear; set to space instead. */ else { surname = ask_one_name (_("Cardholder's surname: ")); givenname = ask_one_name (_("Cardholder's given name: ")); if (!surname || !givenname || (!*surname && !*givenname)) { xfree (surname); xfree (givenname); return gpg_error (GPG_ERR_CANCELED); } isoname = xstrconcat (surname, "<<", givenname, NULL); xfree (surname); xfree (givenname); for (p=isoname; *p; p++) if (*p == ' ') *p = '<'; if (strlen (isoname) > 39 ) { log_info (_("Error: Combined name too long " "(limit is %d characters).\n"), 39); xfree (isoname); goto again; } } err = scd_setattr ("DISP-NAME", isoname, strlen (isoname)); xfree (isoname); return err; } static gpg_error_t cmd_url (card_info_t info, const char *argstr) { gpg_error_t err; char *url; if (!info) return print_help ("URL [--clear]\n\n" "Set the URL data object. That data object can be used by\n" "the FETCH command to retrieve the full public key. The\n" "option --clear deletes the content of that data object.", APP_TYPE_OPENPGP, 0); if (info->apptype != APP_TYPE_OPENPGP) { log_info ("Note: This is an OpenPGP only command.\n"); return gpg_error (GPG_ERR_NOT_SUPPORTED); } if (!strcmp (argstr, "--clear")) url = xstrdup (" "); /* No real way to clear; set to space instead. */ else { url = tty_get (_("URL to retrieve public key: ")); trim_spaces (url); tty_kill_prompt (); if (!*url || *url == CONTROL_D) { err = gpg_error (GPG_ERR_CANCELED); goto leave; } } err = scd_setattr ("PUBKEY-URL", url, strlen (url)); leave: xfree (url); return err; } /* Fetch the key from the URL given on the card or try to get it from * the default keyserver. */ static gpg_error_t cmd_fetch (card_info_t info) { gpg_error_t err; key_info_t kinfo; if (!info) return print_help ("FETCH\n\n" "Retrieve a key using the URL data object or if that is missing\n" "using the fingerprint.", APP_TYPE_OPENPGP, 0); if (info->pubkey_url && *info->pubkey_url) { /* strlist_t sl = NULL; */ /* add_to_strlist (&sl, info.pubkey_url); */ /* err = keyserver_fetch (ctrl, sl, KEYORG_URL); */ /* free_strlist (sl); */ err = gpg_error (GPG_ERR_NOT_IMPLEMENTED); /* FIXME */ } else if ((kinfo = find_kinfo (info, "OPENPGP.1")) && kinfo->fprlen) { /* rc = keyserver_import_fprint (ctrl, info.fpr1, info.fpr1len, */ /* opt.keyserver, 0); */ err = gpg_error (GPG_ERR_NOT_IMPLEMENTED); /* FIXME */ } else err = gpg_error (GPG_ERR_NO_DATA); return err; } static gpg_error_t cmd_login (card_info_t info, char *argstr) { gpg_error_t err; char *data; size_t datalen; int use_default_pin; if (!info) return print_help ("LOGIN [--clear|--use-default-pin] [< FILE]\n\n" "Set the login data object. If FILE is given the data is\n" "is read from that file. This allows for binary data.\n" "The option --clear deletes the login data. --use-default-pin\n" "tells the card to always use the default PIN (\"123456\").", APP_TYPE_OPENPGP, 0); use_default_pin = has_leading_option (argstr, "--use-default-pin"); argstr = skip_options (argstr); if (!strcmp (argstr, "--clear")) { data = xstrdup (" "); /* kludge. */ datalen = 1; } else if (*argstr == '<') /* Read it from a file */ { for (argstr++; spacep (argstr); argstr++) ; err = get_data_from_file (argstr, &data, &datalen); if (err) goto leave; } else { data = tty_get (_("Login data (account name): ")); trim_spaces (data); tty_kill_prompt (); if ((!*data && !use_default_pin) || *data == CONTROL_D) { err = gpg_error (GPG_ERR_CANCELED); goto leave; } datalen = strlen (data); } if (use_default_pin) { char *tmpdata = xmalloc (datalen + 5); memcpy (tmpdata, data, datalen); memcpy (tmpdata+datalen, "\n\x14" "F=3", 5); xfree (data); data = tmpdata; datalen += 5; } err = scd_setattr ("LOGIN-DATA", data, datalen); leave: xfree (data); return err; } static gpg_error_t cmd_lang (card_info_t info, const char *argstr) { gpg_error_t err; char *data, *p; if (!info) return print_help ("LANG [--clear]\n\n" "Change the language info for the card. This info can be used\n" "by applications for a personalized greeting. Up to 4 two-digit\n" "language identifiers can be entered as a preference. The option\n" "--clear removes all identifiers. GnuPG does not use this info.", APP_TYPE_OPENPGP, 0); if (!strcmp (argstr, "--clear")) data = xstrdup (" "); /* Note that we need two spaces here. */ else { again: data = tty_get (_("Language preferences: ")); trim_spaces (data); tty_kill_prompt (); if (!*data || *data == CONTROL_D) { err = gpg_error (GPG_ERR_CANCELED); goto leave; } if (strlen (data) > 8 || (strlen (data) & 1)) { log_info (_("Error: invalid length of preference string.\n")); xfree (data); goto again; } for (p=data; *p && *p >= 'a' && *p <= 'z'; p++) ; if (*p) { log_info (_("Error: invalid characters in preference string.\n")); xfree (data); goto again; } } err = scd_setattr ("DISP-LANG", data, strlen (data)); leave: xfree (data); return err; } static gpg_error_t cmd_salut (card_info_t info, const char *argstr) { gpg_error_t err; char *data = NULL; const char *str; if (!info) return print_help ("SALUT [--clear]\n\n" "Change the salutation info for the card. This info can be used\n" "by applications for a personalized greeting. The option --clear\n" "removes this data object. GnuPG does not use this info.", APP_TYPE_OPENPGP, 0); again: if (!strcmp (argstr, "--clear")) str = "9"; else { data = tty_get (_("Salutation (M = Mr., F = Ms., or space): ")); trim_spaces (data); tty_kill_prompt (); if (*data == CONTROL_D) { err = gpg_error (GPG_ERR_CANCELED); goto leave; } if (!*data) str = "9"; else if ((*data == 'M' || *data == 'm') && !data[1]) str = "1"; else if ((*data == 'F' || *data == 'f') && !data[1]) str = "2"; else { tty_printf (_("Error: invalid response.\n")); xfree (data); data = NULL; goto again; } } err = scd_setattr ("DISP-SEX", str, 1); leave: xfree (data); return err; } static gpg_error_t cmd_cafpr (card_info_t info, char *argstr) { gpg_error_t err; char *data = NULL; const char *s; int i, c; unsigned char fpr[32]; int fprlen; int fprno; int opt_clear = 0; if (!info) return print_help ("CAFPR [--clear] N\n\n" "Change the CA fingerprint number N. N must be in the\n" "range 1 to 3. The option --clear clears the specified\n" "CA fingerprint N or all of them if N is 0 or not given.", APP_TYPE_OPENPGP, 0); opt_clear = has_leading_option (argstr, "--clear"); argstr = skip_options (argstr); if (digitp (argstr)) { fprno = atoi (argstr); while (digitp (argstr)) argstr++; while (spacep (argstr)) argstr++; } else fprno = 0; if (opt_clear && !fprno) ; /* Okay: clear all fprs. */ else if (fprno < 1 || fprno > 3) { err = gpg_error (GPG_ERR_INV_ARG); goto leave; } again: if (opt_clear) { memset (fpr, 0, 20); fprlen = 20; } else { xfree (data); data = tty_get (_("CA fingerprint: ")); trim_spaces (data); tty_kill_prompt (); if (!*data || *data == CONTROL_D) { err = gpg_error (GPG_ERR_CANCELED); goto leave; } for (i=0, s=data; i < sizeof fpr && *s; ) { while (spacep(s)) s++; if (*s == ':') s++; while (spacep(s)) s++; c = hextobyte (s); if (c == -1) break; fpr[i++] = c; s += 2; } fprlen = i; if ((fprlen != 20 && fprlen != 32) || *s) { log_error (_("Error: invalid formatted fingerprint.\n")); goto again; } } if (!fprno) { log_assert (opt_clear); err = scd_setattr ("CA-FPR-1", fpr, fprlen); if (!err) err = scd_setattr ("CA-FPR-2", fpr, fprlen); if (!err) err = scd_setattr ("CA-FPR-3", fpr, fprlen); } else err = scd_setattr (fprno==1?"CA-FPR-1": fprno==2?"CA-FPR-2": fprno==3?"CA-FPR-3":"x", fpr, fprlen); leave: xfree (data); return err; } static gpg_error_t cmd_privatedo (card_info_t info, char *argstr) { gpg_error_t err; int opt_clear; char *do_name = NULL; char *data = NULL; size_t datalen; int do_no; if (!info) return print_help ("PRIVATEDO [--clear] N [< FILE]\n\n" "Change the private data object N. N must be in the\n" "range 1 to 4. If FILE is given the data is is read\n" "from that file. The option --clear clears the data.", APP_TYPE_OPENPGP, 0); opt_clear = has_leading_option (argstr, "--clear"); argstr = skip_options (argstr); if (digitp (argstr)) { do_no = atoi (argstr); while (digitp (argstr)) argstr++; while (spacep (argstr)) argstr++; } else do_no = 0; if (do_no < 1 || do_no > 4) { err = gpg_error (GPG_ERR_INV_ARG); goto leave; } do_name = xasprintf ("PRIVATE-DO-%d", do_no); if (opt_clear) { data = xstrdup (" "); datalen = 1; } else if (*argstr == '<') /* Read it from a file */ { for (argstr++; spacep (argstr); argstr++) ; err = get_data_from_file (argstr, &data, &datalen); if (err) goto leave; } else if (*argstr) { err = gpg_error (GPG_ERR_INV_ARG); goto leave; } else { data = tty_get (_("Private DO data: ")); trim_spaces (data); tty_kill_prompt (); datalen = strlen (data); if (!*data || *data == CONTROL_D) { err = gpg_error (GPG_ERR_CANCELED); goto leave; } } err = scd_setattr (do_name, data, datalen); leave: xfree (do_name); xfree (data); return err; } static gpg_error_t cmd_writecert (card_info_t info, char *argstr) { gpg_error_t err; int opt_clear; int opt_openpgp; char *certref_buffer = NULL; char *certref; char *data = NULL; size_t datalen; estream_t key = NULL; if (!info) return print_help ("WRITECERT CERTREF '<' FILE\n" "WRITECERT --openpgp CERTREF ['<' FILE|FPR]\n" "WRITECERT --clear CERTREF\n\n" "Write a certificate to the card under the id CERTREF.\n" "The option --clear removes the certificate from the card.\n" "The option --openpgp expects an OpenPGP keyblock and stores\n" "it encapsulated in a CMS container; the keyblock is taken\n" "from FILE or directly from the OpenPGP key with FPR", APP_TYPE_OPENPGP, APP_TYPE_PIV, 0); opt_clear = has_leading_option (argstr, "--clear"); opt_openpgp = has_leading_option (argstr, "--openpgp"); argstr = skip_options (argstr); certref = argstr; if ((argstr = strchr (certref, ' '))) { *argstr++ = 0; trim_spaces (certref); trim_spaces (argstr); } else /* Let argstr point to an empty string. */ argstr = certref + strlen (certref); if (info->apptype == APP_TYPE_OPENPGP) { if (!ascii_strcasecmp (certref, "OPENPGP.3") || !strcmp (certref, "3")) certref_buffer = xstrdup ("OPENPGP.3"); else if (!ascii_strcasecmp (certref, "OPENPGP.2")||!strcmp (certref,"2")) certref_buffer = xstrdup ("OPENPGP.2"); else if (!ascii_strcasecmp (certref, "OPENPGP.1")||!strcmp (certref,"1")) certref_buffer = xstrdup ("OPENPGP.1"); else { err = gpg_error (GPG_ERR_INV_ID); log_error ("Error: CERTREF must be OPENPGP.N or just N" " with N being 1..3\""); goto leave; } certref = certref_buffer; } else /* Upcase the certref; prepend cardtype if needed. */ { if (!strchr (certref, '.')) certref_buffer = xstrconcat (app_type_string (info->apptype), ".", certref, NULL); else certref_buffer = xstrdup (certref); ascii_strupr (certref_buffer); certref = certref_buffer; } if (opt_clear) { data = xstrdup (" "); datalen = 1; } else if (*argstr == '<') /* Read it from a file */ { for (argstr++; spacep (argstr); argstr++) ; err = get_data_from_file (argstr, &data, &datalen); if (err) goto leave; if (ascii_memistr (data, datalen, "-----BEGIN CERTIFICATE-----") && ascii_memistr (data, datalen, "-----END CERTIFICATE-----") && !memchr (data, 0, datalen) && !memchr (data, 1, datalen)) { struct b64state b64; err = b64dec_start (&b64, ""); if (!err) err = b64dec_proc (&b64, data, datalen, &datalen); if (!err) err = b64dec_finish (&b64); if (err) goto leave; } } else if (opt_openpgp && *argstr) { err = get_minimal_openpgp_key (&key, argstr); if (err) goto leave; if (es_fclose_snatch (key, (void*)&data, &datalen)) { err = gpg_error_from_syserror (); goto leave; } key = NULL; } else { err = gpg_error (GPG_ERR_INV_ARG); goto leave; } if (opt_openpgp && !opt_clear) { tlv_builder_t tb; void *tmpder; size_t tmpderlen; tb = tlv_builder_new (0); if (!tb) { err = gpg_error_from_syserror (); goto leave; } tlv_builder_add_tag (tb, 0, TAG_SEQUENCE); tlv_builder_add_ptr (tb, 0, TAG_OBJECT_ID, "\x2B\x06\x01\x04\x01\xDA\x47\x02\x03\x01", 10); tlv_builder_add_tag (tb, CLASS_CONTEXT, 0); tlv_builder_add_ptr (tb, 0, TAG_OCTET_STRING, data, datalen); tlv_builder_add_end (tb); tlv_builder_add_end (tb); err = tlv_builder_finalize (tb, &tmpder, &tmpderlen); if (err) goto leave; xfree (data); data = tmpder; datalen = tmpderlen; } err = scd_writecert (certref, data, datalen); leave: es_fclose (key); xfree (data); xfree (certref_buffer); return err; } static gpg_error_t cmd_readcert (card_info_t info, char *argstr) { gpg_error_t err; char *certref_buffer = NULL; char *certref; void *data = NULL; size_t datalen, dataoff; const char *fname; int opt_openpgp; if (!info) return print_help ("READCERT [--openpgp] CERTREF > FILE\n\n" "Read the certificate for key CERTREF and store it in FILE.\n" "With option \"--openpgp\" an OpenPGP keyblock is expected\n" "and stored in FILE.\n", APP_TYPE_OPENPGP, APP_TYPE_PIV, 0); opt_openpgp = has_leading_option (argstr, "--openpgp"); argstr = skip_options (argstr); certref = argstr; if ((argstr = strchr (certref, ' '))) { *argstr++ = 0; trim_spaces (certref); trim_spaces (argstr); } else /* Let argstr point to an empty string. */ argstr = certref + strlen (certref); if (info->apptype == APP_TYPE_OPENPGP) { if (!ascii_strcasecmp (certref, "OPENPGP.3") || !strcmp (certref, "3")) certref_buffer = xstrdup ("OPENPGP.3"); else if (!ascii_strcasecmp (certref, "OPENPGP.2")||!strcmp (certref,"2")) certref_buffer = xstrdup ("OPENPGP.2"); else if (!ascii_strcasecmp (certref, "OPENPGP.1")||!strcmp (certref,"1")) certref_buffer = xstrdup ("OPENPGP.1"); else { err = gpg_error (GPG_ERR_INV_ID); log_error ("Error: CERTREF must be OPENPGP.N or just N" " with N being 1..3\""); goto leave; } certref = certref_buffer; } if (*argstr == '>') /* Write it to a file */ { for (argstr++; spacep (argstr); argstr++) ; fname = argstr; } else { err = gpg_error (GPG_ERR_INV_ARG); goto leave; } dataoff = 0; err = scd_readcert (certref, &data, &datalen); if (err) goto leave; if (opt_openpgp) { /* Check whether DATA contains an OpenPGP keyblock and put only * this into FILE. If the data is something different, return * an error. */ const unsigned char *p; size_t n, objlen, hdrlen; int class, tag, cons, ndef; p = data; n = datalen; if (parse_ber_header (&p,&n,&class,&tag,&cons,&ndef,&objlen,&hdrlen)) goto not_openpgp; if (!(class == CLASS_UNIVERSAL && tag == TAG_SEQUENCE && cons)) goto not_openpgp; /* Does not start with a sequence. */ if (parse_ber_header (&p,&n,&class,&tag,&cons,&ndef,&objlen,&hdrlen)) goto not_openpgp; if (!(class == CLASS_UNIVERSAL && tag == TAG_OBJECT_ID && !cons)) goto not_openpgp; /* No Object ID. */ if (objlen > n) goto not_openpgp; /* Inconsistent lengths. */ if (objlen != 10 || memcmp (p, "\x2B\x06\x01\x04\x01\xDA\x47\x02\x03\x01", objlen)) goto not_openpgp; /* Wrong Object ID. */ p += objlen; n -= objlen; if (parse_ber_header (&p,&n,&class,&tag,&cons,&ndef,&objlen,&hdrlen)) goto not_openpgp; if (!(class == CLASS_CONTEXT && tag == 0 && cons)) goto not_openpgp; /* Not a [0] context tag. */ if (parse_ber_header (&p,&n,&class,&tag,&cons,&ndef,&objlen,&hdrlen)) goto not_openpgp; if (!(class == CLASS_UNIVERSAL && tag == TAG_OCTET_STRING && !cons)) goto not_openpgp; /* Not an octet string. */ if (objlen > n) goto not_openpgp; /* Inconsistent lengths. */ dataoff = p - (const unsigned char*)data; datalen = objlen; } err = put_data_to_file (fname, (unsigned char*)data+dataoff, datalen); goto leave; not_openpgp: err = gpg_error (GPG_ERR_WRONG_BLOB_TYPE); leave: xfree (data); xfree (certref_buffer); return err; } static gpg_error_t cmd_writekey (card_info_t info, char *argstr) { gpg_error_t err; int opt_force; const char *argv[2]; int argc; char *keyref_buffer = NULL; const char *keyref; const char *keygrip; if (!info) return print_help ("WRITEKEY [--force] KEYREF KEYGRIP\n\n" "Write a private key object identified by KEYGRIP to slot KEYREF.\n" "Use --force to overwrite an existing key.", APP_TYPE_OPENPGP, APP_TYPE_PIV, 0); opt_force = has_leading_option (argstr, "--force"); argstr = skip_options (argstr); argc = split_fields (argstr, argv, DIM (argv)); if (argc < 2) { err = gpg_error (GPG_ERR_INV_ARG); goto leave; } /* Upcase the keyref; prepend cardtype if needed. */ keyref = argv[0]; if (!strchr (keyref, '.')) keyref_buffer = xstrconcat (app_type_string (info->apptype), ".", keyref, NULL); else keyref_buffer = xstrdup (keyref); ascii_strupr (keyref_buffer); keyref = keyref_buffer; /* Get the keygrip. */ keygrip = argv[1]; if (strlen (keygrip) != 40 && !(keygrip[0] == '&' && strlen (keygrip+1) == 40)) { log_error (_("Not a valid keygrip (expecting 40 hex digits)\n")); err = gpg_error (GPG_ERR_INV_ARG); goto leave; } err = scd_writekey (keyref, opt_force, keygrip); leave: xfree (keyref_buffer); return err; } static gpg_error_t cmd_forcesig (card_info_t info) { gpg_error_t err; int newstate; if (!info) return print_help ("FORCESIG\n\n" "Toggle the forcesig flag of an OpenPGP card.", APP_TYPE_OPENPGP, 0); if (info->apptype != APP_TYPE_OPENPGP) { log_info ("Note: This is an OpenPGP only command.\n"); return gpg_error (GPG_ERR_NOT_SUPPORTED); } newstate = !info->chv1_cached; err = scd_setattr ("CHV-STATUS-1", newstate? "\x01":"", 1); if (err) goto leave; /* Read it back to be sure we have the right toggle state the next * time. */ err = scd_getattr ("CHV-STATUS", info); leave: return err; } /* Helper for cmd_generate_openpgp. Note that either 0 or 1 is stored at * FORCED_CHV1. */ static gpg_error_t check_pin_for_key_operation (card_info_t info, int *forced_chv1) { gpg_error_t err = 0; *forced_chv1 = !info->chv1_cached; if (*forced_chv1) { /* Switch off the forced mode so that during key generation we * don't get bothered with PIN queries for each self-signature. */ err = scd_setattr ("CHV-STATUS-1", "\x01", 1); if (err) { log_error ("error clearing forced signature PIN flag: %s\n", gpg_strerror (err)); *forced_chv1 = -1; /* Not changed. */ goto leave; } } /* Check the PIN now, so that we won't get asked later for each * binding signature. */ err = scd_checkpin (info->serialno); if (err) log_error ("error checking the PIN: %s\n", gpg_strerror (err)); leave: return err; } /* Helper for cmd_generate_openpgp. */ static void restore_forced_chv1 (int *forced_chv1) { gpg_error_t err; /* Note the possible values stored at FORCED_CHV1: * 0 - forcesig was not enabled. * 1 - forcesig was enabled - enable it again. * -1 - We have not changed anything. */ if (*forced_chv1 == 1) { /* Switch back to forced state. */ err = scd_setattr ("CHV-STATUS-1", "", 1); if (err) log_error ("error setting forced signature PIN flag: %s\n", gpg_strerror (err)); *forced_chv1 = 0; } } /* Ask whether existing keys shall be overwritten. With NULL used for * KINFO it will ask for all keys, other wise for the given key. */ static gpg_error_t ask_replace_keys (key_info_t kinfo) { gpg_error_t err; char *answer; tty_printf ("\n"); if (kinfo) log_info (_("Note: key %s is already stored on the card!\n"), kinfo->keyref); else log_info (_("Note: Keys are already stored on the card!\n")); tty_printf ("\n"); if (kinfo) answer = tty_getf (_("Replace existing key %s ? (y/N) "), kinfo->keyref); else answer = tty_get (_("Replace existing keys? (y/N) ")); tty_kill_prompt (); if (*answer == CONTROL_D) err = gpg_error (GPG_ERR_CANCELED); else if (!answer_is_yes_no_default (answer, 0/*(default to No)*/)) err = gpg_error (GPG_ERR_CANCELED); else err = 0; xfree (answer); return err; } /* Implementation of cmd_generate for OpenPGP cards to generate all * standard keys at once. */ static gpg_error_t generate_all_openpgp_card_keys (card_info_t info, char **algos) { gpg_error_t err; int forced_chv1 = -1; int want_backup; char *answer = NULL; key_info_t kinfo1, kinfo2, kinfo3; if (info->extcap.ki) { xfree (answer); answer = tty_get (_("Make off-card backup of encryption key? (Y/n) ")); want_backup = answer_is_yes_no_default (answer, 1/*(default to Yes)*/); tty_kill_prompt (); if (*answer == CONTROL_D) { err = gpg_error (GPG_ERR_CANCELED); goto leave; } } else want_backup = 0; kinfo1 = find_kinfo (info, "OPENPGP.1"); kinfo2 = find_kinfo (info, "OPENPGP.2"); kinfo3 = find_kinfo (info, "OPENPGP.3"); if ((kinfo1 && kinfo1->fprlen && !mem_is_zero (kinfo1->fpr,kinfo1->fprlen)) || (kinfo2 && kinfo2->fprlen && !mem_is_zero (kinfo2->fpr,kinfo2->fprlen)) || (kinfo3 && kinfo3->fprlen && !mem_is_zero (kinfo3->fpr,kinfo3->fprlen)) ) { err = ask_replace_keys (NULL); if (err) goto leave; } /* If no displayed name has been set, we assume that this is a fresh * card and print a hint about the default PINs. */ if (!info->disp_name || !*info->disp_name) { tty_printf ("\n"); tty_printf (_("Please note that the factory settings of the PINs are\n" " PIN = '%s' Admin PIN = '%s'\n" "You should change them using the command --change-pin\n"), OPENPGP_USER_PIN_DEFAULT, OPENPGP_ADMIN_PIN_DEFAULT); tty_printf ("\n"); } err = check_pin_for_key_operation (info, &forced_chv1); if (err) goto leave; (void)algos; /* FIXME: If we have ALGOS, we need to change the key attr. */ /* FIXME: We need to divert to a function which spawns gpg which * will then create the key. This also requires new features in * gpg. We might also first create the keys on the card and then * tell gpg to use them to create the OpenPGP keyblock. */ /* generate_keypair (ctrl, 1, NULL, info.serialno, want_backup); */ (void)want_backup; err = scd_genkey ("OPENPGP.1", 1, NULL, NULL); leave: restore_forced_chv1 (&forced_chv1); xfree (answer); return err; } /* Create a single key. This is a helper for cmd_generate. */ static gpg_error_t generate_key (card_info_t info, const char *keyref, int force, const char *algo) { gpg_error_t err; key_info_t kinfo; if (info->apptype == APP_TYPE_OPENPGP) { kinfo = find_kinfo (info, keyref); if (!kinfo) { err = gpg_error (GPG_ERR_INV_ID); goto leave; } if (!force && kinfo->fprlen && !mem_is_zero (kinfo->fpr, kinfo->fprlen)) { err = ask_replace_keys (NULL); if (err) goto leave; force = 1; } } err = scd_genkey (keyref, force, algo, NULL); leave: return err; } static gpg_error_t cmd_generate (card_info_t info, char *argstr) { static char * const valid_algos[] = { "rsa2048", "rsa3072", "rsa4096", "", "nistp256", "nistp384", "nistp521", "", "brainpoolP256r1", "brainpoolP384r1", "brainpoolP512r1", "", "ed25519", "cv25519", NULL }; gpg_error_t err; int opt_force; char *p; char **opt_algo = NULL; /* Malloced. */ char *keyref_buffer = NULL; /* Malloced. */ char *keyref; /* Points into argstr or keyref_buffer. */ int i, j; if (!info) return print_help ("GENERATE [--force] [--algo=ALGO{+ALGO2}] KEYREF\n\n" "Create a new key on a card.\n" "Use --force to overwrite an existing key.\n" "Use \"help\" for ALGO to get a list of known algorithms.\n" "For OpenPGP cards several algos may be given.\n" "Note that the OpenPGP key generation is done interactively\n" "unless a single ALGO or KEYREF are given.", APP_TYPE_OPENPGP, APP_TYPE_PIV, 0); if (opt.interactive || opt.verbose) log_info (_("%s card no. %s detected\n"), app_type_string (info->apptype), info->dispserialno? info->dispserialno : info->serialno); opt_force = has_leading_option (argstr, "--force"); err = get_option_value (argstr, "--algo", &p); if (err) goto leave; if (p) { opt_algo = strtokenize (p, "+"); if (!opt_algo) { err = gpg_error_from_syserror (); xfree (p); goto leave; } xfree (p); } argstr = skip_options (argstr); keyref = argstr; if ((argstr = strchr (keyref, ' '))) { *argstr++ = 0; trim_spaces (keyref); trim_spaces (argstr); } else /* Let argstr point to an empty string. */ argstr = keyref + strlen (keyref); if (!*keyref) keyref = NULL; if (*argstr) { /* Extra arguments found. */ err = gpg_error (GPG_ERR_INV_ARG); goto leave; } if (opt_algo) { /* opt_algo is an array of algos. */ for (i=0; opt_algo[i]; i++) { for (j=0; valid_algos[j]; j++) if (*valid_algos[j] && !strcmp (valid_algos[j], opt_algo[i])) break; if (!valid_algos[j]) { int lf = 1; if (!ascii_strcasecmp (opt_algo[i], "help")) log_info ("Known algorithms:\n"); else { log_info ("Invalid algorithm '%s' given. Use one of:\n", opt_algo[i]); err = gpg_error (GPG_ERR_PUBKEY_ALGO); } for (i=0; valid_algos[i]; i++) { if (!*valid_algos[i]) lf = 1; else if (lf) { lf = 0; log_info (" %s%s", valid_algos[i], valid_algos[i+1]?",":"."); } else log_printf (" %s%s", valid_algos[i], valid_algos[i+1]?",":"."); } log_printf ("\n"); show_keysize_warning (); goto leave; } } } /* Upcase the keyref; if it misses the cardtype, prepend it. */ if (keyref) { if (!strchr (keyref, '.')) keyref_buffer = xstrconcat (app_type_string (info->apptype), ".", keyref, NULL); else keyref_buffer = xstrdup (keyref); ascii_strupr (keyref_buffer); keyref = keyref_buffer; } /* Special checks. */ if ((info->cardtype && !strcmp (info->cardtype, "yubikey")) && info->cardversion >= 0x040200 && info->cardversion < 0x040305) { log_error ("On-chip key generation on this YubiKey has been blocked.\n"); log_info ("Please see for details\n"); err = gpg_error (GPG_ERR_NOT_SUPPORTED); goto leave; } /* Divert to dedicated functions. */ if (info->apptype == APP_TYPE_OPENPGP && !keyref && (!opt_algo || (opt_algo[0] && opt_algo[1]))) { /* With no algo requested or more than one algo requested and no * keyref given we create all keys. */ if (opt_force || keyref) log_info ("Note: OpenPGP key generation is interactive.\n"); err = generate_all_openpgp_card_keys (info, opt_algo); } else if (!keyref) err = gpg_error (GPG_ERR_INV_ID); else if (opt_algo && opt_algo[0] && opt_algo[1]) { log_error ("only one algorithm expected as value for --algo.\n"); err = gpg_error (GPG_ERR_INV_ARG); } else err = generate_key (info, keyref, opt_force, opt_algo? opt_algo[0]:NULL); if (!err) { err = scd_learn (info, 0); if (err) log_error ("Error re-reading card: %s\n", gpg_strerror (err)); } leave: xfree (opt_algo); xfree (keyref_buffer); return err; } /* Change a PIN. */ static gpg_error_t cmd_passwd (card_info_t info, char *argstr) { gpg_error_t err = 0; char *answer = NULL; const char *pinref = NULL; int reset_mode = 0; int nullpin = 0; int menu_used = 0; if (!info) return print_help ("PASSWD [--reset|--nullpin] [PINREF]\n\n" "Change or unblock the PINs. Note that in interactive mode\n" "and without a PINREF a menu is presented for certain cards;\n" "in non-interactive and without a PINREF a default value is\n" "used for these cards. The option --reset is used with TCOS\n" "cards to reset the PIN using the PUK or vice versa; --nullpin\n" "is used for these cards to set the initial PIN.", 0); if (opt.interactive || opt.verbose) log_info (_("%s card no. %s detected\n"), app_type_string (info->apptype), info->dispserialno? info->dispserialno : info->serialno); if (has_option (argstr, "--reset")) reset_mode = 1; else if (has_option (argstr, "--nullpin")) nullpin = 1; argstr = skip_options (argstr); /* If --reset or --nullpin has been given we force non-interactive mode. */ if (*argstr || reset_mode || nullpin) { pinref = argstr; if (!*pinref) { err = gpg_error (GPG_ERR_MISSING_VALUE); goto leave; } } else if (opt.interactive && info->apptype == APP_TYPE_OPENPGP) { menu_used = 1; while (!pinref) { xfree (answer); answer = get_selection ("1 - change the PIN\n" "2 - unblock and set new a PIN\n" "3 - change the Admin PIN\n" "4 - set the Reset Code\n" "Q - quit\n"); if (strlen (answer) != 1) continue; else if (*answer == 'q' || *answer == 'Q') goto leave; else if (*answer == '1') pinref = "OPENPGP.1"; else if (*answer == '2') { pinref = "OPENPGP.1"; reset_mode = 1; } else if (*answer == '3') pinref = "OPENPGP.3"; else if (*answer == '4') { pinref = "OPENPGP.2"; reset_mode = 1; } } } else if (info->apptype == APP_TYPE_OPENPGP) pinref = "OPENPGP.1"; else if (opt.interactive && info->apptype == APP_TYPE_PIV) { menu_used = 1; while (!pinref) { xfree (answer); answer = get_selection ("1 - change the PIN\n" "2 - change the PUK\n" "3 - change the Global PIN\n" "Q - quit\n"); if (strlen (answer) != 1) ; else if (*answer == 'q' || *answer == 'Q') goto leave; else if (*answer == '1') pinref = "PIV.80"; else if (*answer == '2') pinref = "PIV.81"; else if (*answer == '3') pinref = "PIV.00"; } } else if (opt.interactive && info->apptype == APP_TYPE_NKS) { int for_qualified = 0; menu_used = 1; log_assert (DIM (info->chvinfo) >= 4); /* If there is a qualified signature use a a menu to select * between standard PIN and QES PINs. */ if (info->chvinfo[2] != -2 || info->chvinfo[3] != -2) { for (;;) { xfree (answer); answer = get_selection (" 1 - Standard PIN/PUK\n" " 2 - PIN/PUK for qualified signature\n" " Q - quit\n"); if (!ascii_strcasecmp (answer, "q")) goto leave; else if (!strcmp (answer, "1")) break; else if (!strcmp (answer, "2")) { for_qualified = 1; break; } } } if (info->chvinfo[for_qualified? 2 : 0] == -4) { while (!pinref) { xfree (answer); answer = get_selection ("The NullPIN is still active on this card.\n" "You need to choose and set a PIN first.\n" "\n" " 1 - Set your PIN\n" " Q - quit\n"); if (!ascii_strcasecmp (answer, "q")) goto leave; else if (!strcmp (answer, "1")) { pinref = for_qualified? "PW1.CH.SIG" : "PW1.CH"; nullpin = 1; } } } else { while (!pinref) { xfree (answer); answer = get_selection (" 1 - change PIN\n" " 2 - reset PIN\n" " 3 - change PUK\n" " 4 - reset PUK\n" " Q - quit\n"); if (!ascii_strcasecmp (answer, "q")) goto leave; else if (!strcmp (answer, "1")) { pinref = for_qualified? "PW1.CH.SIG" : "PW1.CH"; } else if (!strcmp (answer, "2")) { pinref = for_qualified? "PW1.CH.SIG" : "PW1.CH"; reset_mode = 1; } else if (!strcmp (answer, "3")) { pinref = for_qualified? "PW2.CH.SIG" : "PW2.CH"; } else if (!strcmp (answer, "4")) { pinref = for_qualified? "PW2.CH.SIG" : "PW2.CH"; reset_mode = 1; } } } } else if (info->apptype == APP_TYPE_PIV) pinref = "PIV.80"; else { err = gpg_error (GPG_ERR_MISSING_VALUE); goto leave; } err = scd_change_pin (pinref, reset_mode, nullpin); if (err) { if (!opt.interactive && !menu_used && !opt.verbose) ; else if (gpg_err_code (err) == GPG_ERR_CANCELED && gpg_err_source (err) == GPG_ERR_SOURCE_PINENTRY) log_info ("%s\n", gpg_strerror (err)); else if (!ascii_strcasecmp (pinref, "PIV.81")) log_error ("Error changing the PUK.\n"); else if (!ascii_strcasecmp (pinref, "OPENPGP.1") && reset_mode) log_error ("Error unblocking the PIN.\n"); else if (!ascii_strcasecmp (pinref, "OPENPGP.2") && reset_mode) log_error ("Error setting the Reset Code.\n"); else if (!ascii_strcasecmp (pinref, "OPENPGP.3")) log_error ("Error changing the Admin PIN.\n"); else if (reset_mode) log_error ("Error resetting the PIN.\n"); else log_error ("Error changing the PIN.\n"); } else { if (!opt.interactive && !opt.verbose) ; else if (!ascii_strcasecmp (pinref, "PIV.81")) log_info ("PUK changed.\n"); else if (!ascii_strcasecmp (pinref, "OPENPGP.1") && reset_mode) log_info ("PIN unblocked and new PIN set.\n"); else if (!ascii_strcasecmp (pinref, "OPENPGP.2") && reset_mode) log_info ("Reset Code set.\n"); else if (!ascii_strcasecmp (pinref, "OPENPGP.3")) log_info ("Admin PIN changed.\n"); else if (reset_mode) log_info ("PIN reset.\n"); else log_info ("PIN changed.\n"); /* Update the CHV status. */ err = scd_getattr ("CHV-STATUS", info); } leave: xfree (answer); return err; } static gpg_error_t cmd_unblock (card_info_t info) { gpg_error_t err = 0; if (!info) return print_help ("UNBLOCK\n\n" "Unblock a PIN using a PUK or Reset Code. Note that OpenPGP\n" "cards prior to version 2 can't use this; instead the PASSWD\n" "command can be used to set a new PIN.", 0); if (opt.interactive || opt.verbose) log_info (_("%s card no. %s detected\n"), app_type_string (info->apptype), info->dispserialno? info->dispserialno : info->serialno); if (info->apptype == APP_TYPE_OPENPGP) { if (!info->is_v2) { log_error (_("This command is only available for version 2 cards\n")); err = gpg_error (GPG_ERR_NOT_SUPPORTED); } else if (!info->chvinfo[1]) { log_error (_("Reset Code not or not anymore available\n")); err = gpg_error (GPG_ERR_PIN_BLOCKED); } else { err = scd_change_pin ("OPENPGP.2", 0, 0); if (!err) log_info ("PIN changed.\n"); } } else if (info->apptype == APP_TYPE_PIV) { /* Unblock the Application PIN. */ err = scd_change_pin ("PIV.80", 1, 0); if (!err) log_info ("PIN unblocked and changed.\n"); } else { log_info ("Unblocking not supported for '%s'.\n", app_type_string (info->apptype)); err = gpg_error (GPG_ERR_NOT_IMPLEMENTED); } return err; } /* Note: On successful execution a redisplay should be scheduled. If * this function fails the card may be in an unknown state. */ static gpg_error_t cmd_factoryreset (card_info_t info) { gpg_error_t err; char *answer = NULL; int termstate = 0; int any_apdu = 0; int is_yubikey = 0; int locked = 0; int i; if (!info) return print_help ("FACTORY-RESET\n\n" "Do a complete reset of some OpenPGP and PIV cards. This\n" "deletes all data and keys and resets the PINs to their default.\n" "This is mainly used by developers with scratch cards. Don't\n" "worry, you need to confirm before the command proceeds.", APP_TYPE_OPENPGP, APP_TYPE_PIV, 0); /* We support the factory reset for most OpenPGP cards and Yubikeys * with the PIV application. */ if (info->apptype == APP_TYPE_OPENPGP) ; else if (info->apptype == APP_TYPE_PIV && info->cardtype && !strcmp (info->cardtype, "yubikey")) is_yubikey = 1; else return gpg_error (GPG_ERR_NOT_SUPPORTED); /* For an OpenPGP card the code below basically does the same what * this gpg-connect-agent script does: * * scd reset * scd serialno undefined * scd apdu 00 A4 04 00 06 D2 76 00 01 24 01 * scd apdu 00 20 00 81 08 40 40 40 40 40 40 40 40 * scd apdu 00 20 00 81 08 40 40 40 40 40 40 40 40 * scd apdu 00 20 00 81 08 40 40 40 40 40 40 40 40 * scd apdu 00 20 00 81 08 40 40 40 40 40 40 40 40 * scd apdu 00 20 00 83 08 40 40 40 40 40 40 40 40 * scd apdu 00 20 00 83 08 40 40 40 40 40 40 40 40 * scd apdu 00 20 00 83 08 40 40 40 40 40 40 40 40 * scd apdu 00 20 00 83 08 40 40 40 40 40 40 40 40 * scd apdu 00 e6 00 00 * scd apdu 00 44 00 00 * scd reset * /echo Card has been reset to factory defaults * * For a PIV application on a Yubikey it merely issues the Yubikey * specific resset command. */ err = scd_learn (info, 0); if (gpg_err_code (err) == GPG_ERR_OBJ_TERM_STATE && gpg_err_source (err) == GPG_ERR_SOURCE_SCD) termstate = 1; else if (err) { log_error (_("OpenPGP card not available: %s\n"), gpg_strerror (err)); goto leave; } if (opt.interactive || opt.verbose) log_info (_("%s card no. %s detected\n"), app_type_string (info->apptype), info->dispserialno? info->dispserialno : info->serialno); if (!termstate || is_yubikey) { if (!is_yubikey) { if (!(info->status_indicator == 3 || info->status_indicator == 5)) { /* Note: We won't see status-indicator 3 here because it * is not possible to select a card application in * termination state. */ log_error (_("This command is not supported by this card\n")); err = gpg_error (GPG_ERR_NOT_SUPPORTED); goto leave; } } tty_printf ("\n"); log_info (_("Note: This command destroys all keys stored on the card!\n")); tty_printf ("\n"); xfree (answer); answer = tty_get (_("Continue? (y/N) ")); tty_kill_prompt (); trim_spaces (answer); if (*answer == CONTROL_D || !answer_is_yes_no_default (answer, 0/*(default to no)*/)) { err = gpg_error (GPG_ERR_CANCELED); goto leave; } xfree (answer); answer = tty_get (_("Really do a factory reset? (enter \"yes\") ")); tty_kill_prompt (); trim_spaces (answer); if (strcmp (answer, "yes") && strcmp (answer,_("yes"))) { err = gpg_error (GPG_ERR_CANCELED); goto leave; } if (is_yubikey) { /* If the PIV application is already selected, we only need to * send the special reset APDU after having blocked PIN and * PUK. Note that blocking the PUK is done using the * unblock PIN command. */ any_apdu = 1; for (i=0; i < 5; i++) send_apdu ("0020008008FFFFFFFFFFFFFFFF", "VERIFY", 0xffff, NULL, NULL); for (i=0; i < 5; i++) send_apdu ("002C008010FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "RESET RETRY COUNTER", 0xffff, NULL, NULL); err = send_apdu ("00FB000001FF", "YUBIKEY RESET", 0, NULL, NULL); if (err) goto leave; } else /* OpenPGP card. */ { any_apdu = 1; /* We need to select a card application before we can send * APDUs to the card without scdaemon doing anything on its * own. We then lock the connection so that other tools * (e.g. Kleopatra) don't try a new select. */ err = send_apdu ("lock", "locking connection ", 0, NULL, NULL); if (err) goto leave; locked = 1; err = send_apdu ("reset-keep-lock", "reset", 0, NULL, NULL); if (err) goto leave; err = send_apdu ("undefined", "dummy select ", 0, NULL, NULL); if (err) goto leave; /* Select the OpenPGP application. */ err = send_apdu ("00A4040006D27600012401", "SELECT AID", 0, NULL, NULL); if (err) goto leave; /* Do some dummy verifies with wrong PINs to set the retry * counter to zero. We can't easily use the card version 2.1 * feature of presenting the admin PIN to allow the terminate * command because there is no machinery in scdaemon to catch * the verify command and ask for the PIN when the "APDU" * command is used. * Here, the length of dummy wrong PIN is 32-byte, also * supporting authentication with KDF DO. */ for (i=0; i < 4; i++) send_apdu ("0020008120" "40404040404040404040404040404040" "40404040404040404040404040404040", "VERIFY", 0xffff, NULL, NULL); for (i=0; i < 4; i++) send_apdu ("0020008320" "40404040404040404040404040404040" "40404040404040404040404040404040", "VERIFY", 0xffff, NULL, NULL); /* Send terminate datafile command. */ err = send_apdu ("00e60000", "TERMINATE DF", 0x6985, NULL, NULL); if (err) goto leave; } } if (!is_yubikey) { any_apdu = 1; /* Send activate datafile command. This is used without * confirmation if the card is already in termination state. */ err = send_apdu ("00440000", "ACTIVATE DF", 0, NULL, NULL); if (err) goto leave; } /* Finally we reset the card reader once more. */ if (locked) err = send_apdu ("reset-keep-lock", "reset", 0, NULL, NULL); else err = send_apdu (NULL, "RESET", 0, NULL, NULL); if (err) goto leave; /* Then, connect the card again. */ err = scd_serialno (NULL, NULL); if (!err) info->need_sn_cmd = 0; leave: if (err && any_apdu && !is_yubikey) { log_info ("Due to an error the card might be in an inconsistent state\n" "You should run the LIST command to check this.\n"); /* FIXME: We need a better solution in the case that the card is * in a termination state, i.e. the card was removed before the * activate was sent. The best solution I found with v2.1 * Zeitcontrol card was to kill scdaemon and the issue this * sequence with gpg-connect-agent: * scd reset * scd serialno undefined * scd apdu 00A4040006D27600012401 (returns error) * scd apdu 00440000 * Then kill scdaemon again and issue: * scd reset * scd serialno openpgp */ } if (locked) send_apdu ("unlock", "unlocking connection ", 0, NULL, NULL); xfree (answer); return err; } /* Generate KDF data. This is a helper for cmd_kdfsetup. */ static gpg_error_t gen_kdf_data (unsigned char *data, int single_salt) { gpg_error_t err; const unsigned char h0[] = { 0x81, 0x01, 0x03, 0x82, 0x01, 0x08, 0x83, 0x04 }; const unsigned char h1[] = { 0x84, 0x08 }; const unsigned char h2[] = { 0x85, 0x08 }; const unsigned char h3[] = { 0x86, 0x08 }; const unsigned char h4[] = { 0x87, 0x20 }; const unsigned char h5[] = { 0x88, 0x20 }; unsigned char *p, *salt_user, *salt_admin; unsigned char s2k_char; unsigned int iterations; unsigned char count_4byte[4]; p = data; s2k_char = encode_s2k_iterations (agent_get_s2k_count ()); iterations = S2K_DECODE_COUNT (s2k_char); count_4byte[0] = (iterations >> 24) & 0xff; count_4byte[1] = (iterations >> 16) & 0xff; count_4byte[2] = (iterations >> 8) & 0xff; count_4byte[3] = (iterations & 0xff); memcpy (p, h0, sizeof h0); p += sizeof h0; memcpy (p, count_4byte, sizeof count_4byte); p += sizeof count_4byte; memcpy (p, h1, sizeof h1); salt_user = (p += sizeof h1); gcry_randomize (p, 8, GCRY_STRONG_RANDOM); p += 8; if (single_salt) salt_admin = salt_user; else { memcpy (p, h2, sizeof h2); p += sizeof h2; gcry_randomize (p, 8, GCRY_STRONG_RANDOM); p += 8; memcpy (p, h3, sizeof h3); salt_admin = (p += sizeof h3); gcry_randomize (p, 8, GCRY_STRONG_RANDOM); p += 8; } memcpy (p, h4, sizeof h4); p += sizeof h4; err = gcry_kdf_derive (OPENPGP_USER_PIN_DEFAULT, strlen (OPENPGP_USER_PIN_DEFAULT), GCRY_KDF_ITERSALTED_S2K, GCRY_MD_SHA256, salt_user, 8, iterations, 32, p); p += 32; if (!err) { memcpy (p, h5, sizeof h5); p += sizeof h5; err = gcry_kdf_derive (OPENPGP_ADMIN_PIN_DEFAULT, strlen (OPENPGP_ADMIN_PIN_DEFAULT), GCRY_KDF_ITERSALTED_S2K, GCRY_MD_SHA256, salt_admin, 8, iterations, 32, p); } return err; } static gpg_error_t cmd_kdfsetup (card_info_t info, char *argstr) { gpg_error_t err; unsigned char kdf_data[OPENPGP_KDF_DATA_LENGTH_MAX]; int single = (*argstr != 0); if (!info) return print_help ("KDF-SETUP\n\n" "Prepare the OpenPGP card KDF feature for this card.", APP_TYPE_OPENPGP, 0); if (info->apptype != APP_TYPE_OPENPGP) { log_info ("Note: This is an OpenPGP only command.\n"); return gpg_error (GPG_ERR_NOT_SUPPORTED); } if (!info->extcap.kdf) { log_error (_("This command is not supported by this card\n")); err = gpg_error (GPG_ERR_NOT_SUPPORTED); goto leave; } err = gen_kdf_data (kdf_data, single); if (err) goto leave; err = scd_setattr ("KDF", kdf_data, single ? OPENPGP_KDF_DATA_LENGTH_MIN /* */ : OPENPGP_KDF_DATA_LENGTH_MAX); if (err) goto leave; err = scd_getattr ("KDF", info); leave: return err; } static void show_keysize_warning (void) { static int shown; if (shown) return; shown = 1; tty_printf (_("Note: There is no guarantee that the card supports the requested\n" " key type or size. If the key generation does not succeed,\n" " please check the documentation of your card to see which\n" " key types and sizes are supported.\n") ); } static gpg_error_t cmd_uif (card_info_t info, char *argstr) { gpg_error_t err; int keyno; char name[50]; unsigned char data[2]; char *answer = NULL; int opt_yes; if (!info) return print_help ("UIF N [on|off|permanent]\n\n" "Change the User Interaction Flag. N must in the range 1 to 3.", APP_TYPE_OPENPGP, APP_TYPE_PIV, 0); if (!info->extcap.bt) { log_error (_("This command is not supported by this card\n")); err = gpg_error (GPG_ERR_NOT_SUPPORTED); goto leave; } opt_yes = has_leading_option (argstr, "--yes"); argstr = skip_options (argstr); if (digitp (argstr)) { keyno = atoi (argstr); while (digitp (argstr)) argstr++; while (spacep (argstr)) argstr++; } else keyno = 0; if (keyno < 1 || keyno > 3) { err = gpg_error (GPG_ERR_INV_ARG); goto leave; } if ( !strcmp (argstr, "off") ) data[0] = 0x00; else if ( !strcmp (argstr, "on") ) data[0] = 0x01; else if ( !strcmp (argstr, "permanent") ) data[0] = 0x02; else { err = gpg_error (GPG_ERR_INV_ARG); goto leave; } data[1] = 0x20; log_assert (keyno - 1 < DIM(info->uif)); if (info->uif[keyno-1] == 2) { log_info (_("User Interaction Flag is set to \"%s\" - can't change\n"), "permanent"); err = gpg_error (GPG_ERR_INV_STATE); goto leave; } if (data[0] == 0x02) { if (opt.interactive) { tty_printf (_("Warning: Setting the User Interaction Flag to \"%s\"\n" " can only be reverted using a factory reset!\n" ), "permanent"); answer = tty_get (_("Continue? (y/N) ")); tty_kill_prompt (); if (*answer == CONTROL_D) err = gpg_error (GPG_ERR_CANCELED); else if (!answer_is_yes_no_default (answer, 0/*(default to No)*/)) err = gpg_error (GPG_ERR_CANCELED); else err = 0; } else if (!opt_yes) { log_info (_("Warning: Setting the User Interaction Flag to \"%s\"\n" " can only be reverted using a factory reset!\n" ), "permanent"); log_info (_("Please use \"uif --yes %d %s\"\n"), keyno, "permanent"); err = gpg_error (GPG_ERR_CANCELED); } else err = 0; if (err) goto leave; } snprintf (name, sizeof name, "UIF-%d", keyno); err = scd_setattr (name, data, 2); if (!err) /* Read all UIF attributes again. */ err = scd_getattr ("UIF", info); leave: xfree (answer); return err; } static gpg_error_t cmd_yubikey (card_info_t info, char *argstr) { gpg_error_t err, err2; estream_t fp = opt.interactive? NULL : es_stdout; const char *words[20]; int nwords; if (!info) return print_help ("YUBIKEY args\n\n" "Various commands pertaining to Yubikey tokens with being:\n" "\n" " LIST \n" "\n" "List supported and enabled applications.\n" "\n" " ENABLE usb|nfc|all [otp|u2f|opgp|piv|oath|fido2|all]\n" " DISABLE usb|nfc|all [otp|u2f|opgp|piv|oath|fido2|all]\n" "\n" "Enable or disable the specified or all applications on the\n" "given interface.", 0); argstr = skip_options (argstr); if (!info->cardtype || strcmp (info->cardtype, "yubikey")) { log_info ("This command can only be used with Yubikeys.\n"); err = gpg_error (GPG_ERR_NOT_SUPPORTED); goto leave; } nwords = split_fields (argstr, words, DIM (words)); if (nwords < 1) { err = gpg_error (GPG_ERR_SYNTAX); goto leave; } /* Note that we always do a learn to get a chance to the card back * into a usable state. */ err = yubikey_commands (info, fp, nwords, words); err2 = scd_learn (info, 0); if (err2) log_error ("Error re-reading card: %s\n", gpg_strerror (err2)); leave: return err; } static gpg_error_t cmd_apdu (card_info_t info, char *argstr) { gpg_error_t err; estream_t fp = opt.interactive? NULL : es_stdout; int with_atr; int handle_more; const char *s; const char *exlenstr; int exlenstrlen; char *options = NULL; unsigned int sw; unsigned char *result = NULL; size_t i, j, resultlen; if (!info) return print_help ("APDU [--more] [--exlen[=N]] \n" "\n" "Send an APDU to the current card. This command bypasses the high\n" "level functions and sends the data directly to the card. HEXSTRING\n" "is expected to be a proper APDU.\n" "\n" "Using the option \"--more\" handles the card status word MORE_DATA\n" "(61xx) and concatenates all responses to one block.\n" "\n" "Using the option \"--exlen\" the returned APDU may use extended\n" "length up to N bytes. If N is not given a default value is used.\n", 0); if (has_option (argstr, "--dump-atr")) with_atr = 2; else with_atr = has_option (argstr, "--atr"); handle_more = has_option (argstr, "--more"); exlenstr = has_option_name (argstr, "--exlen"); exlenstrlen = 0; if (exlenstr) { for (s=exlenstr; *s && !spacep (s); s++) exlenstrlen++; } argstr = skip_options (argstr); if (with_atr || handle_more || exlenstr) options = xasprintf ("%s%s%s%.*s", with_atr == 2? " --dump-atr": with_atr? " --data-atr":"", handle_more?" --more":"", exlenstr?" --exlen=":"", exlenstrlen, exlenstr?exlenstr:""); err = scd_apdu (argstr, options, &sw, &result, &resultlen); if (err) goto leave; if (!with_atr) { if (opt.interactive || opt.verbose) { char *p = scd_apdu_strerror (sw); log_info ("Statusword: 0x%04x (%s)\n", sw, p? p: "?"); xfree (p); } else log_info ("Statusword: 0x%04x\n", sw); } for (i=0; i < resultlen; ) { size_t save_i = i; tty_fprintf (fp, "D[%04X] ", (unsigned int)i); for (j=0; j < 16 ; j++, i++) { if (j == 8) tty_fprintf (fp, " "); if (i < resultlen) tty_fprintf (fp, " %02X", result[i]); else tty_fprintf (fp, " "); } tty_fprintf (fp, " "); i = save_i; for (j=0; j < 16; j++, i++) { unsigned int c = result[i]; if ( i >= resultlen ) tty_fprintf (fp, " "); else if (isascii (c) && isprint (c) && !iscntrl (c)) tty_fprintf (fp, "%c", c); else tty_fprintf (fp, "."); } tty_fprintf (fp, "\n"); } leave: xfree (result); xfree (options); return err; } static gpg_error_t cmd_gpg (card_info_t info, char *argstr, int use_gpgsm) { gpg_error_t err; char **argarray; ccparray_t ccp; const char **argv = NULL; - pid_t pid; + gnupg_process_t proc; int i; if (!info) return print_help ("GPG[SM] \n" "\n" "Run gpg/gpgsm directly from this shell.\n", 0); /* Fixme: We need to write and use a version of strtokenize which * takes care of shell-style quoting. */ argarray = strtokenize (argstr, " \t\n\v"); if (!argarray) { err = gpg_error_from_syserror (); goto leave; } ccparray_init (&ccp, 0); for (i=0; argarray[i]; i++) ccparray_put (&ccp, argarray[i]); argv = ccparray_get (&ccp, NULL); if (!argv) { err = gpg_error_from_syserror (); goto leave; } - err = gnupg_spawn_process (use_gpgsm? opt.gpgsm_program:opt.gpg_program, - argv, NULL, (GNUPG_SPAWN_KEEP_STDOUT - |GNUPG_SPAWN_KEEP_STDERR), - NULL, NULL, NULL, &pid); + err = gnupg_process_spawn (use_gpgsm? opt.gpgsm_program:opt.gpg_program, + argv, + (GNUPG_PROCESS_STDOUT_KEEP + | GNUPG_PROCESS_STDERR_KEEP), + NULL, NULL, &proc); if (!err) { - err = gnupg_wait_process (use_gpgsm? opt.gpgsm_program:opt.gpg_program, - pid, 1, NULL); - gnupg_release_process (pid); + err = gnupg_process_wait (proc, 1); + gnupg_process_release (proc); } leave: xfree (argv); xfree (argarray); return err; } static gpg_error_t cmd_history (card_info_t info, char *argstr) { int opt_list, opt_clear; opt_list = has_option (argstr, "--list"); opt_clear = has_option (argstr, "--clear"); if (!info || !(opt_list || opt_clear)) return print_help ("HISTORY --list\n" " List the command history\n" "HISTORY --clear\n" " Clear the command history", 0); if (opt_list) tty_printf ("Sorry, history listing not yet possible\n"); if (opt_clear) tty_read_history (NULL, 0); return 0; } /* Data used by the command parser. This needs to be outside of the * function scope to allow readline based command completion. */ enum cmdids { cmdNOP = 0, cmdQUIT, cmdHELP, cmdLIST, cmdRESET, cmdVERIFY, cmdNAME, cmdURL, cmdFETCH, cmdLOGIN, cmdLANG, cmdSALUT, cmdCAFPR, cmdFORCESIG, cmdGENERATE, cmdPASSWD, cmdPRIVATEDO, cmdWRITECERT, cmdREADCERT, cmdWRITEKEY, cmdUNBLOCK, cmdFACTRST, cmdKDFSETUP, cmdUIF, cmdAUTH, cmdYUBIKEY, cmdAPDU, cmdGPG, cmdGPGSM, cmdHISTORY, cmdINVCMD }; static struct { const char *name; enum cmdids id; const char *desc; } cmds[] = { { "quit" , cmdQUIT, N_("quit this menu")}, { "q" , cmdQUIT, NULL }, { "bye" , cmdQUIT, NULL }, { "help" , cmdHELP, N_("show this help")}, { "?" , cmdHELP, NULL }, { "list" , cmdLIST, N_("list all available data")}, { "l" , cmdLIST, NULL }, { "name" , cmdNAME, N_("change card holder's name")}, { "url" , cmdURL, N_("change URL to retrieve key")}, { "fetch" , cmdFETCH, N_("fetch the key specified in the card URL")}, { "login" , cmdLOGIN, N_("change the login name")}, { "lang" , cmdLANG, N_("change the language preferences")}, { "salutation",cmdSALUT, N_("change card holder's salutation")}, { "salut" , cmdSALUT, NULL }, { "cafpr" , cmdCAFPR , N_("change a CA fingerprint")}, { "forcesig", cmdFORCESIG, N_("toggle the signature force PIN flag")}, { "generate", cmdGENERATE, N_("generate new keys")}, { "passwd" , cmdPASSWD, N_("menu to change or unblock the PIN")}, { "verify" , cmdVERIFY, N_("verify the PIN and list all data")}, { "unblock" , cmdUNBLOCK, N_("unblock the PIN using a Reset Code")}, { "authenticate",cmdAUTH, N_("authenticate to the card")}, { "auth" , cmdAUTH, NULL }, { "reset" , cmdRESET, N_("send a reset to the card daemon")}, { "factory-reset",cmdFACTRST, N_("destroy all keys and data")}, { "kdf-setup", cmdKDFSETUP, N_("setup KDF for PIN authentication")}, { "uif", cmdUIF, N_("change the User Interaction Flag")}, { "privatedo", cmdPRIVATEDO, N_("change a private data object")}, { "readcert", cmdREADCERT, N_("read a certificate from a data object")}, { "writecert", cmdWRITECERT, N_("store a certificate to a data object")}, { "writekey", cmdWRITEKEY, N_("store a private key to a data object")}, { "yubikey", cmdYUBIKEY, N_("Yubikey management commands")}, { "gpg", cmdGPG, NULL}, { "gpgsm", cmdGPGSM, NULL}, { "apdu", cmdAPDU, NULL}, { "history", cmdHISTORY, N_("manage the command history")}, { NULL, cmdINVCMD, NULL } }; /* The command line command dispatcher. */ static gpg_error_t dispatch_command (card_info_t info, const char *orig_command) { gpg_error_t err = 0; enum cmdids cmd; /* The command. */ char *command; /* A malloced copy of ORIG_COMMAND. */ char *argstr; /* The argument as a string. */ int i; int ignore_error; if ((ignore_error = *orig_command == '-')) orig_command++; command = xstrdup (orig_command); argstr = NULL; if ((argstr = strchr (command, ' '))) { *argstr++ = 0; trim_spaces (command); trim_spaces (argstr); } for (i=0; cmds[i].name; i++ ) if (!ascii_strcasecmp (command, cmds[i].name )) break; cmd = cmds[i].id; /* (If not found this will be cmdINVCMD). */ /* Make sure we have valid strings for the args. They are allowed * to be modified and must thus point to a buffer. */ if (!argstr) argstr = command + strlen (command); /* For most commands we need to make sure that we have a card. */ if (!info) ; /* Help mode */ else if (!(cmd == cmdNOP || cmd == cmdQUIT || cmd == cmdHELP || cmd == cmdINVCMD) && !info->initialized) { err = scd_learn (info, 0); if (err) { err = fixup_scd_errors (err); log_error ("Error reading card: %s\n", gpg_strerror (err)); goto leave; } } if (info) info->card_removed = 0; switch (cmd) { case cmdNOP: if (!info) print_help ("NOP\n\n" "Dummy command.", 0); break; case cmdQUIT: if (!info) print_help ("QUIT\n\n" "Stop processing.", 0); else { err = gpg_error (GPG_ERR_EOF); goto leave; } break; case cmdHELP: if (!info) print_help ("HELP [command]\n\n" "Show all commands. With an argument show help\n" "for that command.", 0); else if (*argstr) dispatch_command (NULL, argstr); else { es_printf ("List of commands (\"help \" for details):\n"); for (i=0; cmds[i].name; i++ ) if(cmds[i].desc) es_printf("%-14s %s\n", cmds[i].name, _(cmds[i].desc) ); es_printf ("Prefix a command with a dash to ignore its error.\n"); } break; case cmdRESET: if (!info) print_help ("RESET\n\n" "Send a RESET to the card daemon.", 0); else { flush_keyblock_cache (); err = scd_apdu (NULL, NULL, NULL, NULL, NULL); if (!err) info->need_sn_cmd = 1; } break; case cmdLIST: err = cmd_list (info, argstr); break; case cmdVERIFY: err = cmd_verify (info, argstr); break; case cmdAUTH: err = cmd_authenticate (info, argstr); break; case cmdNAME: err = cmd_name (info, argstr); break; case cmdURL: err = cmd_url (info, argstr); break; case cmdFETCH: err = cmd_fetch (info); break; case cmdLOGIN: err = cmd_login (info, argstr); break; case cmdLANG: err = cmd_lang (info, argstr); break; case cmdSALUT: err = cmd_salut (info, argstr); break; case cmdCAFPR: err = cmd_cafpr (info, argstr); break; case cmdPRIVATEDO: err = cmd_privatedo (info, argstr); break; case cmdWRITECERT: err = cmd_writecert (info, argstr); break; case cmdREADCERT: err = cmd_readcert (info, argstr); break; case cmdWRITEKEY: err = cmd_writekey (info, argstr); break; case cmdFORCESIG: err = cmd_forcesig (info); break; case cmdGENERATE: err = cmd_generate (info, argstr); break; case cmdPASSWD: err = cmd_passwd (info, argstr); break; case cmdUNBLOCK: err = cmd_unblock (info); break; case cmdFACTRST: err = cmd_factoryreset (info); break; case cmdKDFSETUP: err = cmd_kdfsetup (info, argstr); break; case cmdUIF: err = cmd_uif (info, argstr); break; case cmdYUBIKEY: err = cmd_yubikey (info, argstr); break; case cmdAPDU: err = cmd_apdu (info, argstr); break; case cmdGPG: err = cmd_gpg (info, argstr, 0); break; case cmdGPGSM: err = cmd_gpg (info, argstr, 1); break; case cmdHISTORY: err = 0; break; /* Only used in interactive mode. */ case cmdINVCMD: default: log_error (_("Invalid command (try \"help\")\n")); break; } /* End command switch. */ leave: /* Return GPG_ERR_EOF only if its origin was "quit". */ es_fflush (es_stdout); if (gpg_err_code (err) == GPG_ERR_EOF && cmd != cmdQUIT) err = gpg_error (GPG_ERR_GENERAL); if (!err && info && info->card_removed) { info->card_removed = 0; info->need_sn_cmd = 1; err = gpg_error (GPG_ERR_CARD_REMOVED); } if (err && gpg_err_code (err) != GPG_ERR_EOF) { err = fixup_scd_errors (err); if (ignore_error) { log_info ("Command '%s' failed: %s\n", command, gpg_strerror (err)); err = 0; } else { log_error ("Command '%s' failed: %s\n", command, gpg_strerror (err)); if (gpg_err_code (err) == GPG_ERR_CARD_NOT_PRESENT) info->need_sn_cmd = 1; } } xfree (command); return err; } /* The interactive main loop. */ static void interactive_loop (void) { gpg_error_t err; char *answer = NULL; /* The input line. */ enum cmdids cmd = cmdNOP; /* The command. */ char *argstr; /* The argument as a string. */ int redisplay = 1; /* Whether to redisplay the main info. */ char *help_arg = NULL; /* Argument of the HELP command. */ struct card_info_s info_buffer = { 0 }; card_info_t info = &info_buffer; char *p; int i; char *historyname = NULL; /* In the interactive mode we do not want to print the program prefix. */ log_set_prefix (NULL, 0); if (!opt.no_history) { historyname = make_filename (gnupg_homedir (), HISTORYNAME, NULL); if (tty_read_history (historyname, 500)) log_info ("error reading '%s': %s\n", historyname, gpg_strerror (gpg_error_from_syserror ())); } for (;;) { if (help_arg) { /* Clear info to indicate helpmode */ info = NULL; } else if (!info) { /* Get out of help. */ info = &info_buffer; help_arg = NULL; redisplay = 0; } else if (redisplay) { err = cmd_list (info, ""); if (err) { err = fixup_scd_errors (err); log_error ("Error reading card: %s\n", gpg_strerror (err)); } else { tty_printf("\n"); redisplay = 0; } } if (!info) { /* Copy the pending help arg into our answer. Note that * help_arg points into answer. */ p = xstrdup (help_arg); help_arg = NULL; xfree (answer); answer = p; } else { do { xfree (answer); tty_enable_completion (command_completion); answer = tty_get (_("gpg/card> ")); tty_kill_prompt(); tty_disable_completion (); trim_spaces(answer); } while ( *answer == '#' ); } argstr = NULL; if (!*answer) cmd = cmdLIST; /* We default to the list command */ else if (*answer == CONTROL_D) cmd = cmdQUIT; else { if ((argstr = strchr (answer,' '))) { *argstr++ = 0; trim_spaces (answer); trim_spaces (argstr); } for (i=0; cmds[i].name; i++ ) if (!ascii_strcasecmp (answer, cmds[i].name )) break; cmd = cmds[i].id; } /* Make sure we have valid strings for the args. They are * allowed to be modified and must thus point to a buffer. */ if (!argstr) argstr = answer + strlen (answer); if (!(cmd == cmdNOP || cmd == cmdQUIT || cmd == cmdHELP || cmd == cmdHISTORY || cmd == cmdINVCMD)) { /* If redisplay is set we know that there was an error reading * the card. In this case we force a LIST command to retry. */ if (!info) ; /* In help mode. */ else if (redisplay) { cmd = cmdLIST; } else if (!info->serialno) { /* Without a serial number most commands won't work. * Catch it here. */ if (cmd == cmdRESET || cmd == cmdLIST) info->need_sn_cmd = 1; else { tty_printf ("\n"); tty_printf ("Serial number missing\n"); continue; } } } if (info) info->card_removed = 0; err = 0; switch (cmd) { case cmdNOP: if (!info) print_help ("NOP\n\n" "Dummy command.", 0); break; case cmdQUIT: if (!info) print_help ("QUIT\n\n" "Leave this tool.", 0); else { tty_printf ("\n"); goto leave; } break; case cmdHELP: if (!info) print_help ("HELP [command]\n\n" "Show all commands. With an argument show help\n" "for that command.", 0); else if (*argstr) help_arg = argstr; /* Trigger help for a command. */ else { tty_printf ("List of commands (\"help \" for details):\n"); for (i=0; cmds[i].name; i++ ) if(cmds[i].desc) tty_printf("%-14s %s\n", cmds[i].name, _(cmds[i].desc) ); } break; case cmdRESET: if (!info) print_help ("RESET\n\n" "Send a RESET to the card daemon.", 0); else { flush_keyblock_cache (); err = scd_apdu (NULL, NULL, NULL, NULL, NULL); if (!err) info->need_sn_cmd = 1; } break; case cmdLIST: err = cmd_list (info, argstr); break; case cmdVERIFY: err = cmd_verify (info, argstr); if (!err) redisplay = 1; break; case cmdAUTH: err = cmd_authenticate (info, argstr); break; case cmdNAME: err = cmd_name (info, argstr); break; case cmdURL: err = cmd_url (info, argstr); break; case cmdFETCH: err = cmd_fetch (info); break; case cmdLOGIN: err = cmd_login (info, argstr); break; case cmdLANG: err = cmd_lang (info, argstr); break; case cmdSALUT: err = cmd_salut (info, argstr); break; case cmdCAFPR: err = cmd_cafpr (info, argstr); break; case cmdPRIVATEDO: err = cmd_privatedo (info, argstr); break; case cmdWRITECERT: err = cmd_writecert (info, argstr); break; case cmdREADCERT: err = cmd_readcert (info, argstr); break; case cmdWRITEKEY: err = cmd_writekey (info, argstr); break; case cmdFORCESIG: err = cmd_forcesig (info); break; case cmdGENERATE: err = cmd_generate (info, argstr); break; case cmdPASSWD: err = cmd_passwd (info, argstr); break; case cmdUNBLOCK: err = cmd_unblock (info); break; case cmdFACTRST: err = cmd_factoryreset (info); if (!err) redisplay = 1; break; case cmdKDFSETUP: err = cmd_kdfsetup (info, argstr); break; case cmdUIF: err = cmd_uif (info, argstr); break; case cmdYUBIKEY: err = cmd_yubikey (info, argstr); break; case cmdAPDU: err = cmd_apdu (info, argstr); break; case cmdGPG: err = cmd_gpg (info, argstr, 0); break; case cmdGPGSM: err = cmd_gpg (info, argstr, 1); break; case cmdHISTORY: err = cmd_history (info, argstr); break; case cmdINVCMD: default: tty_printf ("\n"); tty_printf (_("Invalid command (try \"help\")\n")); break; } /* End command switch. */ if (!err && info && info->card_removed) { info->card_removed = 0; info->need_sn_cmd = 1; err = gpg_error (GPG_ERR_CARD_REMOVED); } if (gpg_err_code (err) == GPG_ERR_CANCELED) tty_fprintf (NULL, "\n"); else if (err) { const char *s = "?"; for (i=0; cmds[i].name; i++ ) if (cmd == cmds[i].id) { s = cmds[i].name; break; } err = fixup_scd_errors (err); log_error ("Command '%s' failed: %s\n", s, gpg_strerror (err)); if (gpg_err_code (err) == GPG_ERR_CARD_NOT_PRESENT) info->need_sn_cmd = 1; } } /* End of main menu loop. */ leave: if (historyname && tty_write_history (historyname)) log_info ("error writing '%s': %s\n", historyname, gpg_strerror (gpg_error_from_syserror ())); release_card_info (info); xfree (historyname); xfree (answer); } #ifdef HAVE_LIBREADLINE /* Helper function for readline's command completion. */ static char * command_generator (const char *text, int state) { static int list_index, len; const char *name; /* If this is a new word to complete, initialize now. This includes * saving the length of TEXT for efficiency, and initializing the index variable to 0. */ if (!state) { list_index = 0; len = strlen(text); } /* Return the next partial match */ while ((name = cmds[list_index].name)) { /* Only complete commands that have help text. */ if (cmds[list_index++].desc && !strncmp (name, text, len)) return strdup(name); } return NULL; } /* Second helper function for readline's command completion. */ static char ** command_completion (const char *text, int start, int end) { (void)end; /* If we are at the start of a line, we try and command-complete. * If not, just do nothing for now. The support for help completion * needs to be more smarter. */ if (!start) return rl_completion_matches (text, command_generator); else if (start == 5 && !ascii_strncasecmp (rl_line_buffer, "help ", 5)) return rl_completion_matches (text, command_generator); rl_attempted_completion_over = 1; return NULL; } #endif /*HAVE_LIBREADLINE*/ diff --git a/tools/gpgconf-comp.c b/tools/gpgconf-comp.c index 90f2f53d3..d6aa9d61b 100644 --- a/tools/gpgconf-comp.c +++ b/tools/gpgconf-comp.c @@ -1,3607 +1,3628 @@ /* gpgconf-comp.c - Configuration utility for GnuPG. * Copyright (C) 2004, 2007-2011 Free Software Foundation, Inc. * Copyright (C) 2016 Werner Koch * Copyright (C) 2020, 2021 g10 Code GmbH * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GnuPG; if not, see . * SPDX-License-Identifier: GPL-3.0-or-later */ #if HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #ifdef HAVE_SIGNAL_H # include #endif #include #ifdef HAVE_W32_SYSTEM # define WIN32_LEAN_AND_MEAN 1 # include #else # include # include #endif #include "../common/util.h" #include "../common/i18n.h" #include "../common/exechelp.h" #include "../common/sysutils.h" #include "../common/status.h" #include "../common/gc-opt-flags.h" #include "gpgconf.h" #if (__GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 5 )) void gc_error (int status, int errnum, const char *fmt, ...) \ __attribute__ ((format (printf, 3, 4))); #endif /* Output a diagnostic message. If ERRNUM is not 0, then the output is followed by a colon, a white space, and the error string for the error number ERRNUM. In any case the output is finished by a newline. The message is prepended by the program name, a colon, and a whitespace. The output may be further formatted or redirected by the jnlib logging facility. */ void gc_error (int status, int errnum, const char *fmt, ...) { va_list arg_ptr; va_start (arg_ptr, fmt); log_logv (GPGRT_LOGLVL_ERROR, fmt, arg_ptr); va_end (arg_ptr); if (errnum) log_printf (": %s\n", strerror (errnum)); else log_printf ("\n"); if (status) { log_printf (NULL); log_printf ("fatal error (exit status %i)\n", status); gpgconf_failure (gpg_error_from_errno (errnum)); } } /* Forward declaration. */ static void gpg_agent_runtime_change (int killflag); static void scdaemon_runtime_change (int killflag); #ifdef BUILD_WITH_TPM2D static void tpm2daemon_runtime_change (int killflag); #endif static void dirmngr_runtime_change (int killflag); static void keyboxd_runtime_change (int killflag); /* STRING_ARRAY is a malloced array with malloced strings. It is used * a space to store strings so that other objects may point to these * strings. It shall never be shrinked or any items changes. * STRING_ARRAY itself may be reallocated to increase the size of the * table. STRING_ARRAY_USED is the number of items currently used, * STRING_ARRAY_SIZE is the number of calloced slots. */ static char **string_array; static size_t string_array_used; static size_t string_array_size; /* Option configuration. */ /* An option might take an argument, or not. Argument types can be basic or complex. Basic types are generic and easy to validate. Complex types provide more specific information about the intended use, but can be difficult to validate. If you add to this enum, don't forget to update GC_ARG_TYPE below. YOU MUST NOT CHANGE THE NUMBERS OF THE EXISTING ENTRIES, AS THEY ARE PART OF THE EXTERNAL INTERFACE. */ typedef enum { /* Basic argument types. */ /* No argument. */ GC_ARG_TYPE_NONE = 0, /* A String argument. */ GC_ARG_TYPE_STRING = 1, /* A signed integer argument. */ GC_ARG_TYPE_INT32 = 2, /* An unsigned integer argument. */ GC_ARG_TYPE_UINT32 = 3, /* ADD NEW BASIC TYPE ENTRIES HERE. */ /* Complex argument types. */ /* A complete filename. */ GC_ARG_TYPE_FILENAME = 32, /* An LDAP server in the format HOSTNAME:PORT:USERNAME:PASSWORD:BASE_DN. */ GC_ARG_TYPE_LDAP_SERVER = 33, /* A 40 character fingerprint. */ GC_ARG_TYPE_KEY_FPR = 34, /* A user ID or key ID or fingerprint for a certificate. */ GC_ARG_TYPE_PUB_KEY = 35, /* A user ID or key ID or fingerprint for a certificate with a key. */ GC_ARG_TYPE_SEC_KEY = 36, /* A alias list made up of a key, an equal sign and a space separated list of values. */ GC_ARG_TYPE_ALIAS_LIST = 37, /* ADD NEW COMPLEX TYPE ENTRIES HERE. */ /* The number of the above entries. */ GC_ARG_TYPE_NR } gc_arg_type_t; /* For every argument, we record some information about it in the following struct. */ static const struct { /* For every argument type exists a basic argument type that can be used as a fallback for input and validation purposes. */ gc_arg_type_t fallback; /* Human-readable name of the type. */ const char *name; } gc_arg_type[GC_ARG_TYPE_NR] = { /* The basic argument types have their own types as fallback. */ { GC_ARG_TYPE_NONE, "none" }, { GC_ARG_TYPE_STRING, "string" }, { GC_ARG_TYPE_INT32, "int32" }, { GC_ARG_TYPE_UINT32, "uint32" }, /* Reserved basic type entries for future extension. */ { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, { GC_ARG_TYPE_NR, NULL }, /* The complex argument types have a basic type as fallback. */ { GC_ARG_TYPE_STRING, "filename" }, { GC_ARG_TYPE_STRING, "ldap server" }, { GC_ARG_TYPE_STRING, "key fpr" }, { GC_ARG_TYPE_STRING, "pub key" }, { GC_ARG_TYPE_STRING, "sec key" }, { GC_ARG_TYPE_STRING, "alias list" }, }; /* Every option has an associated expert level, than can be used to hide advanced and expert options from beginners. If you add to this list, don't forget to update GC_LEVEL below. YOU MUST NOT CHANGE THE NUMBERS OF THE EXISTING ENTRIES, AS THEY ARE PART OF THE EXTERNAL INTERFACE. */ typedef enum { /* The basic options should always be displayed. */ GC_LEVEL_BASIC, /* The advanced options may be hidden from beginners. */ GC_LEVEL_ADVANCED, /* The expert options should only be displayed to experts. */ GC_LEVEL_EXPERT, /* The invisible options should normally never be displayed. */ GC_LEVEL_INVISIBLE, /* The internal options are never exported, they mark options that are recorded for internal use only. */ GC_LEVEL_INTERNAL, /* ADD NEW ENTRIES HERE. */ /* The number of the above entries. */ GC_LEVEL_NR } gc_expert_level_t; /* A description for each expert level. */ static const struct { const char *name; } gc_level[] = { { "basic" }, { "advanced" }, { "expert" }, { "invisible" }, { "internal" } }; /* Option flags. The flags which are used by the components are defined by gc-opt-flags.h, included above. YOU MUST NOT CHANGE THE NUMBERS OF THE EXISTING FLAGS, AS THEY ARE PART OF THE EXTERNAL INTERFACE. */ /* Some entries in the emitted option list are not options, but mark the beginning of a new group of options. These entries have the GROUP flag set. Note that this is internally also known as a header line. */ #define GC_OPT_FLAG_GROUP (1UL << 0) /* The ARG_OPT flag for an option indicates that the argument is optional. This is never set for GC_ARG_TYPE_NONE options. */ #define GC_OPT_FLAG_ARG_OPT (1UL << 1) /* The LIST flag for an option indicates that the option can occur several times. A comma separated list of arguments is used as the argument value. */ #define GC_OPT_FLAG_LIST (1UL << 2) /* The RUNTIME flag for an option indicates that the option can be changed at runtime. */ #define GC_OPT_FLAG_RUNTIME (1UL << 3) /* A human-readable description for each flag. */ static const struct { const char *name; } gc_flag[] = { { "group" }, { "optional arg" }, { "list" }, { "runtime" }, { "default" }, { "default desc" }, { "no arg desc" }, { "no change" } }; /* Each option we want to support in gpgconf has the needed * information in a static list per componenet. This struct describes * the info for a single option. */ struct known_option_s { /* If this is NULL, then this is a terminator in an array of unknown * length. Otherwise it is the name of the option described by this * entry. The name must not contain a colon. */ const char *name; /* The option flags. */ unsigned long flags; /* The expert level. */ gc_expert_level_t level; /* The complex type of the option argument; the default of 0 is used * for a standard type as returned by --dump-option-table. */ gc_arg_type_t arg_type; }; typedef struct known_option_s known_option_t; /* The known options of the GC_COMPONENT_GPG_AGENT component. */ static known_option_t known_options_gpg_agent[] = { { "verbose", GC_OPT_FLAG_LIST|GC_OPT_FLAG_RUNTIME, GC_LEVEL_BASIC }, { "quiet", GC_OPT_FLAG_NONE|GC_OPT_FLAG_RUNTIME, GC_LEVEL_BASIC }, { "disable-scdaemon", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "enable-ssh-support", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "ssh-fingerprint-digest", GC_OPT_FLAG_RUNTIME, GC_LEVEL_EXPERT }, { "enable-putty-support", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "enable-extended-key-format", GC_OPT_FLAG_RUNTIME, GC_LEVEL_INVISIBLE }, { "debug-level", GC_OPT_FLAG_ARG_OPT|GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED}, { "log-file", GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED, /**/ GC_ARG_TYPE_FILENAME }, { "faked-system-time", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "default-cache-ttl", GC_OPT_FLAG_RUNTIME, GC_LEVEL_BASIC }, { "default-cache-ttl-ssh", GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED }, { "max-cache-ttl", GC_OPT_FLAG_RUNTIME, GC_LEVEL_EXPERT }, { "max-cache-ttl-ssh", GC_OPT_FLAG_RUNTIME, GC_LEVEL_EXPERT }, { "ignore-cache-for-signing", GC_OPT_FLAG_RUNTIME, GC_LEVEL_BASIC }, { "allow-emacs-pinentry", GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED }, { "grab", GC_OPT_FLAG_RUNTIME, GC_LEVEL_EXPERT }, { "no-allow-external-cache", GC_OPT_FLAG_RUNTIME, GC_LEVEL_BASIC }, { "no-allow-mark-trusted", GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED }, { "no-allow-loopback-pinentry", GC_OPT_FLAG_RUNTIME, GC_LEVEL_EXPERT }, { "enforce-passphrase-constraints", GC_OPT_FLAG_RUNTIME, GC_LEVEL_EXPERT }, { "min-passphrase-len", GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED }, { "min-passphrase-nonalpha", GC_OPT_FLAG_RUNTIME, GC_LEVEL_EXPERT }, { "check-passphrase-pattern", GC_OPT_FLAG_RUNTIME, GC_LEVEL_EXPERT, /**/ GC_ARG_TYPE_FILENAME }, { "check-sym-passphrase-pattern", GC_OPT_FLAG_RUNTIME, GC_LEVEL_EXPERT, /**/ GC_ARG_TYPE_FILENAME }, { "max-passphrase-days", GC_OPT_FLAG_RUNTIME, GC_LEVEL_EXPERT }, { "enable-passphrase-history", GC_OPT_FLAG_RUNTIME, GC_LEVEL_EXPERT }, { "pinentry-timeout", GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED }, { NULL } }; /* The known options of the GC_COMPONENT_SCDAEMON component. */ static known_option_t known_options_scdaemon[] = { { "verbose", GC_OPT_FLAG_LIST|GC_OPT_FLAG_RUNTIME, GC_LEVEL_BASIC }, { "quiet", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "no-greeting", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "reader-port", GC_OPT_FLAG_RUNTIME, GC_LEVEL_BASIC }, { "ctapi-driver", GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED }, { "pcsc-driver", GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED }, { "disable-ccid", GC_OPT_FLAG_RUNTIME, GC_LEVEL_EXPERT }, { "disable-pinpad", GC_OPT_FLAG_NONE|GC_OPT_FLAG_RUNTIME, GC_LEVEL_BASIC }, { "enable-pinpad-varlen", GC_OPT_FLAG_RUNTIME, GC_LEVEL_BASIC }, { "card-timeout", GC_OPT_FLAG_RUNTIME, GC_LEVEL_BASIC }, { "application-priority", GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED }, { "debug-level", GC_OPT_FLAG_ARG_OPT|GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED}, { "log-file", GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED, GC_ARG_TYPE_FILENAME }, { "deny-admin", GC_OPT_FLAG_RUNTIME, GC_LEVEL_BASIC }, { NULL } }; #ifdef BUILD_WITH_TPM2D /* The known options of the GC_COMPONENT_TPM2DAEMON component. */ static known_option_t known_options_tpm2daemon[] = { { "verbose", GC_OPT_FLAG_LIST|GC_OPT_FLAG_RUNTIME, GC_LEVEL_BASIC }, { "quiet", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "no-greeting", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "debug-level", GC_OPT_FLAG_ARG_OPT|GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED}, { "log-file", GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED, GC_ARG_TYPE_FILENAME }, { "deny-admin", GC_OPT_FLAG_RUNTIME, GC_LEVEL_BASIC }, { "parent", GC_OPT_FLAG_RUNTIME, GC_LEVEL_ADVANCED }, { NULL } }; #endif /* The known options of the GC_COMPONENT_GPG component. */ static known_option_t known_options_gpg[] = { { "verbose", GC_OPT_FLAG_LIST, GC_LEVEL_BASIC }, { "quiet", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "no-greeting", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "default-key", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "encrypt-to", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "group", GC_OPT_FLAG_LIST, GC_LEVEL_ADVANCED, GC_ARG_TYPE_ALIAS_LIST}, { "compliance", GC_OPT_FLAG_NONE, GC_LEVEL_EXPERT }, { "default-new-key-algo", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "trust-model", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "debug-level", GC_OPT_FLAG_ARG_OPT, GC_LEVEL_ADVANCED }, { "log-file", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED, GC_ARG_TYPE_FILENAME }, { "keyserver", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "auto-key-locate", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "auto-key-import", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "auto-key-retrieve", GC_OPT_FLAG_NONE, GC_LEVEL_EXPERT }, { "include-key-block", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "disable-dirmngr", GC_OPT_FLAG_NONE, GC_LEVEL_EXPERT }, { "max-cert-depth", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "completes-needed", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "marginals-needed", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, /* The next items are pseudo options which we read via --gpgconf-list. * The meta information is taken from the table below. */ { "default_pubkey_algo", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "compliance_de_vs", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "use_keyboxd", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { NULL } }; static const char *known_pseudo_options_gpg[] = {/* v-- ARGPARSE_TYPE_STRING */ "default_pubkey_algo:0:2:@:", /* A basic compliance check for gpg. We use gpg here but the * result is valid for all components. * v-- ARGPARSE_TYPE_INT */ "compliance_de_vs:0:1:@:", /* True is use_keyboxd is enabled. That option can be set in * common.conf but is not direcly supported by gpgconf. Thus we * only allow to read it out. * v-- ARGPARSE_TYPE_INT */ "use_keyboxd:0:1:@:", NULL }; /* The known options of the GC_COMPONENT_GPGSM component. */ static known_option_t known_options_gpgsm[] = { { "verbose", GC_OPT_FLAG_LIST, GC_LEVEL_BASIC }, { "quiet", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "no-greeting", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "default-key", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "encrypt-to", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "disable-dirmngr", GC_OPT_FLAG_NONE, GC_LEVEL_EXPERT }, { "p12-charset", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "keyserver", GC_OPT_FLAG_LIST, GC_LEVEL_INVISIBLE, GC_ARG_TYPE_LDAP_SERVER }, { "compliance", GC_OPT_FLAG_NONE, GC_LEVEL_EXPERT }, { "debug-level", GC_OPT_FLAG_ARG_OPT, GC_LEVEL_ADVANCED }, { "log-file", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED, GC_ARG_TYPE_FILENAME }, { "faked-system-time", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "disable-crl-checks", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "enable-crl-checks", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "enable-ocsp", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "include-certs", GC_OPT_FLAG_NONE, GC_LEVEL_EXPERT }, { "disable-policy-checks", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "auto-issuer-key-retrieve", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "cipher-algo", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "disable-trusted-cert-crl-check", GC_OPT_FLAG_NONE, GC_LEVEL_EXPERT }, /* Pseudo option follows. See also table below. */ { "default_pubkey_algo", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { NULL } }; static const char *known_pseudo_options_gpgsm[] = {/* v-- ARGPARSE_TYPE_STRING */ "default_pubkey_algo:0:2:@:", NULL }; /* The known options of the GC_COMPONENT_DIRMNGR component. */ static known_option_t known_options_dirmngr[] = { { "verbose", GC_OPT_FLAG_LIST, GC_LEVEL_BASIC }, { "quiet", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "no-greeting", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "resolver-timeout", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "nameserver", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "debug-level", GC_OPT_FLAG_ARG_OPT, GC_LEVEL_ADVANCED }, { "log-file", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED, GC_ARG_TYPE_FILENAME }, { "faked-system-time", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { "batch", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "force", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "use-tor", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "keyserver", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "ldapserver", GC_OPT_FLAG_LIST, GC_LEVEL_BASIC, GC_ARG_TYPE_LDAP_SERVER }, { "disable-http", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "ignore-http-dp", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "http-proxy", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "honor-http-proxy", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "disable-ldap", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "ignore-ldap-dp", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "ldap-proxy", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "only-ldap-proxy", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "add-servers", GC_OPT_FLAG_NONE, GC_LEVEL_EXPERT }, { "ldaptimeout", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "max-replies", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "allow-ocsp", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "ocsp-responder", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "ocsp-signer", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { "allow-version-check", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "ignore-ocsp-service-url", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED }, { NULL } }; /* The known options of the GC_COMPONENT_KEYBOXD component. */ static known_option_t known_options_keyboxd[] = { { "verbose", GC_OPT_FLAG_LIST, GC_LEVEL_BASIC }, { "quiet", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC }, { "log-file", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED, GC_ARG_TYPE_FILENAME }, { "faked-system-time", GC_OPT_FLAG_NONE, GC_LEVEL_INVISIBLE }, { NULL } }; /* The known options of the GC_COMPONENT_PINENTRY component. */ static known_option_t known_options_pinentry[] = { { NULL } }; /* Our main option info object. We copy all required information from the * gpgrt_opt_t items but convert the flags value to bit flags. */ struct gc_option_s { const char *name; /* The same as gpgrt_opt_t.long_opt. */ const char *desc; /* The same as gpgrt_opt_t.description. */ unsigned int is_header:1; /* This is a header item. */ unsigned int is_list:1; /* This is a list style option. */ unsigned int opt_arg:1; /* The option's argument is optional. */ unsigned int runtime:1; /* The option is runtime changeable. */ unsigned int gpgconf_list:1; /* Mentioned by --gpgconf-list. */ unsigned int has_default:1; /* The option has a default value. */ unsigned int def_in_desc:1; /* The default is in the descrition. */ unsigned int no_arg_desc:1; /* The argument has a default ???. */ unsigned int no_change:1; /* User shall not change the option. */ unsigned int attr_ignore:1; /* The ARGPARSE_ATTR_IGNORE. */ unsigned int attr_force:1; /* The ARGPARSE_ATTR_FORCE. */ /* The expert level - copied from known_options. */ gc_expert_level_t level; /* The complex type - copied from known_options. */ gc_arg_type_t arg_type; /* The default value for this option. This is NULL if the option is not present in the component, the empty string if no default is available, and otherwise a quoted string. This is currently malloced.*/ char *default_value; /* The current value of this option. */ char *value; /* The new flags for this option. The only defined flag is actually GC_OPT_FLAG_DEFAULT, and it means that the option should be deleted. In this case, NEW_VALUE is NULL. */ unsigned long new_flags; /* The new value of this option. */ char *new_value; }; typedef struct gc_option_s gc_option_t; /* The information associated with each component. */ static struct { /* The name of the component. Some components don't have an * associated program, but are implemented directly by GPGConf. In * this case, PROGRAM is NULL. */ char *program; /* The displayed name of this component. Must not contain a colon * (':') character. */ const char *name; /* The gettext domain for the description DESC. If this is NULL, then the description is not translated. */ const char *desc_domain; /* The description of this component. */ const char *desc; /* The module name (GNUPG_MODULE_NAME_foo) as defined by * ../common/util.h. This value is used to get the actual installed * path of the program. 0 is used if no program for the component * is available. */ char module_name; /* The name for the configuration filename of this component. */ const char *option_config_filename; /* The static table of known options for this component. */ known_option_t *known_options; /* The static table of known pseudo options for this component or NULL. */ const char **known_pseudo_options; /* The runtime change callback. If KILLFLAG is true the component is killed and not just reloaded. */ void (*runtime_change) (int killflag); /* The table of known options as read from the component including * header lines and such. This is suitable to be passed to * gpgrt_argparser. Will be filled in by * retrieve_options_from_program. */ gpgrt_opt_t *opt_table; /* The full table including data from OPT_TABLE. The end of the * table is marked by NULL entry for NAME. Will be filled in by * retrieve_options_from_program. */ gc_option_t *options; } gc_component[GC_COMPONENT_NR] = { /* Note: The order of the items must match the order given in the * gc_component_id_t enumeration. The order is often used by * frontends to display the backend options thus do not change the * order without considering the user experience. */ { NULL }, /* DUMMY for GC_COMPONENT_ANY */ { GPG_NAME, GPG_DISP_NAME, "gnupg", N_("OpenPGP"), GNUPG_MODULE_NAME_GPG, GPG_NAME ".conf", known_options_gpg, known_pseudo_options_gpg }, { GPGSM_NAME, GPGSM_DISP_NAME, "gnupg", N_("S/MIME"), GNUPG_MODULE_NAME_GPGSM, GPGSM_NAME ".conf", known_options_gpgsm, known_pseudo_options_gpgsm }, { KEYBOXD_NAME, KEYBOXD_DISP_NAME, "gnupg", N_("Public Keys"), GNUPG_MODULE_NAME_KEYBOXD, KEYBOXD_NAME ".conf", known_options_keyboxd, NULL, keyboxd_runtime_change }, { GPG_AGENT_NAME, GPG_AGENT_DISP_NAME, "gnupg", N_("Private Keys"), GNUPG_MODULE_NAME_AGENT, GPG_AGENT_NAME ".conf", known_options_gpg_agent, NULL, gpg_agent_runtime_change }, { SCDAEMON_NAME, SCDAEMON_DISP_NAME, "gnupg", N_("Smartcards"), GNUPG_MODULE_NAME_SCDAEMON, SCDAEMON_NAME ".conf", known_options_scdaemon, NULL, scdaemon_runtime_change}, #ifdef BUILD_WITH_TPM2D { TPM2DAEMON_NAME, TPM2DAEMON_DISP_NAME, "gnupg", N_("TPM"), GNUPG_MODULE_NAME_TPM2DAEMON, TPM2DAEMON_NAME ".conf", known_options_tpm2daemon, NULL, tpm2daemon_runtime_change}, #else { NULL }, /* DUMMY to keep the table in-sync with enums */ #endif { DIRMNGR_NAME, DIRMNGR_DISP_NAME, "gnupg", N_("Network"), GNUPG_MODULE_NAME_DIRMNGR, DIRMNGR_NAME ".conf", known_options_dirmngr, NULL, dirmngr_runtime_change }, { "pinentry", "Pinentry", "gnupg", N_("Passphrase Entry"), GNUPG_MODULE_NAME_PINENTRY, NULL, known_options_pinentry } }; /* Structure used to collect error output of the component programs. */ struct error_line_s; typedef struct error_line_s *error_line_t; struct error_line_s { error_line_t next; /* Link to next item. */ const char *fname; /* Name of the config file (points into BUFFER). */ unsigned int lineno; /* Line number of the config file. */ const char *errtext; /* Text of the error message (points into BUFFER). */ char buffer[1]; /* Helper buffer. */ }; /* Initialization and finalization. */ static void gc_option_free (gc_option_t *o) { if (o == NULL || o->name == NULL) return; xfree (o->value); gc_option_free (o + 1); } static void gc_components_free (void) { int i; for (i = 0; i < DIM (gc_component); i++) gc_option_free (gc_component[i].options); } void gc_components_init (void) { atexit (gc_components_free); } /* Engine specific support. */ static void gpg_agent_runtime_change (int killflag) { gpg_error_t err = 0; const char *pgmname; const char *argv[5]; - pid_t pid = (pid_t)(-1); + gnupg_process_t proc = NULL; int i = 0; int cmdidx; pgmname = gnupg_module_name (GNUPG_MODULE_NAME_CONNECT_AGENT); if (!gnupg_default_homedir_p ()) { argv[i++] = "--homedir"; argv[i++] = gnupg_homedir (); } argv[i++] = "--no-autostart"; cmdidx = i; argv[i++] = killflag? "KILLAGENT" : "RELOADAGENT"; argv[i] = NULL; log_assert (i < DIM(argv)); if (!err) - err = gnupg_spawn_process_fd (pgmname, argv, -1, -1, -1, &pid); + err = gnupg_process_spawn (pgmname, argv, 0, NULL, NULL, &proc); if (!err) - err = gnupg_wait_process (pgmname, pid, 1, NULL); + err = gnupg_process_wait (proc, 1); if (err) gc_error (0, 0, "error running '%s %s': %s", pgmname, argv[cmdidx], gpg_strerror (err)); - gnupg_release_process (pid); + gnupg_process_release (proc); } static void scdaemon_runtime_change (int killflag) { gpg_error_t err = 0; const char *pgmname; const char *argv[9]; - pid_t pid = (pid_t)(-1); + gnupg_process_t proc = NULL; int i = 0; int cmdidx; (void)killflag; /* For scdaemon kill and reload are synonyms. */ /* We use "GETINFO app_running" to see whether the agent is already running and kill it only in this case. This avoids an explicit starting of the agent in case it is not yet running. There is obviously a race condition but that should not harm too much. */ pgmname = gnupg_module_name (GNUPG_MODULE_NAME_CONNECT_AGENT); if (!gnupg_default_homedir_p ()) { argv[i++] = "--homedir"; argv[i++] = gnupg_homedir (); } argv[i++] = "-s"; argv[i++] = "--no-autostart"; argv[i++] = "GETINFO scd_running"; argv[i++] = "/if ${! $?}"; cmdidx = i; argv[i++] = "scd killscd"; argv[i++] = "/end"; argv[i] = NULL; log_assert (i < DIM(argv)); if (!err) - err = gnupg_spawn_process_fd (pgmname, argv, -1, -1, -1, &pid); + err = gnupg_process_spawn (pgmname, argv, 0, NULL, NULL, &proc); if (!err) - err = gnupg_wait_process (pgmname, pid, 1, NULL); + err = gnupg_process_wait (proc, 1); if (err) gc_error (0, 0, "error running '%s %s': %s", pgmname, argv[cmdidx], gpg_strerror (err)); - gnupg_release_process (pid); + gnupg_process_release (proc); } #ifdef BUILD_WITH_TPM2D static void tpm2daemon_runtime_change (int killflag) { gpg_error_t err = 0; const char *pgmname; const char *argv[9]; - pid_t pid = (pid_t)(-1); + gnupg_process_t proc = NULL; int i = 0; int cmdidx; (void)killflag; /* For scdaemon kill and reload are synonyms. */ /* We use "GETINFO app_running" to see whether the agent is already running and kill it only in this case. This avoids an explicit starting of the agent in case it is not yet running. There is obviously a race condition but that should not harm too much. */ pgmname = gnupg_module_name (GNUPG_MODULE_NAME_CONNECT_AGENT); if (!gnupg_default_homedir_p ()) { argv[i++] = "--homedir"; argv[i++] = gnupg_homedir (); } argv[i++] = "-s"; argv[i++] = "--no-autostart"; argv[i++] = "GETINFO tpm2d_running"; argv[i++] = "/if ${! $?}"; cmdidx = i; argv[i++] = "scd killtpm2cd"; argv[i++] = "/end"; argv[i] = NULL; log_assert (i < DIM(argv)); if (!err) - err = gnupg_spawn_process_fd (pgmname, argv, -1, -1, -1, &pid); + err = gnupg_process_spawn (pgmname, argv, 0, NULL, NULL, &proc); if (!err) - err = gnupg_wait_process (pgmname, pid, 1, NULL); + err = gnupg_process_wait (proc, 1); if (err) gc_error (0, 0, "error running '%s %s': %s", pgmname, argv[cmdidx], gpg_strerror (err)); - gnupg_release_process (pid); + gnupg_process_release (proc); } #endif static void dirmngr_runtime_change (int killflag) { gpg_error_t err = 0; const char *pgmname; const char *argv[6]; - pid_t pid = (pid_t)(-1); + gnupg_process_t proc = NULL; int i = 0; int cmdidx; pgmname = gnupg_module_name (GNUPG_MODULE_NAME_CONNECT_AGENT); if (!gnupg_default_homedir_p ()) { argv[i++] = "--homedir"; argv[i++] = gnupg_homedir (); } argv[i++] = "--no-autostart"; argv[i++] = "--dirmngr"; cmdidx = i; argv[i++] = killflag? "KILLDIRMNGR" : "RELOADDIRMNGR"; argv[i] = NULL; log_assert (i < DIM(argv)); if (!err) - err = gnupg_spawn_process_fd (pgmname, argv, -1, -1, -1, &pid); + err = gnupg_process_spawn (pgmname, argv, 0, NULL, NULL, &proc); if (!err) - err = gnupg_wait_process (pgmname, pid, 1, NULL); + err = gnupg_process_wait (proc, 1); if (err) gc_error (0, 0, "error running '%s %s': %s", pgmname, argv[cmdidx], gpg_strerror (err)); - gnupg_release_process (pid); + gnupg_process_release (proc); } static void keyboxd_runtime_change (int killflag) { gpg_error_t err = 0; const char *pgmname; const char *argv[6]; - pid_t pid = (pid_t)(-1); + gnupg_process_t proc = NULL; int i = 0; int cmdidx; pgmname = gnupg_module_name (GNUPG_MODULE_NAME_CONNECT_AGENT); argv[i++] = "--no-autostart"; argv[i++] = "--keyboxd"; cmdidx = i; argv[i++] = killflag? "KILLKEYBOXD" : "RELOADKEYBOXD"; if (!gnupg_default_homedir_p ()) { argv[i++] = "--homedir"; argv[i++] = gnupg_homedir (); } argv[i] = NULL; log_assert (i < DIM(argv)); if (!err) - err = gnupg_spawn_process_fd (pgmname, argv, -1, -1, -1, &pid); + err = gnupg_process_spawn (pgmname, argv, 0, NULL, NULL, &proc); if (!err) - err = gnupg_wait_process (pgmname, pid, 1, NULL); + err = gnupg_process_wait (proc, 1); if (err) gc_error (0, 0, "error running '%s %s': %s", pgmname, argv[cmdidx], gpg_strerror (err)); - gnupg_release_process (pid); + gnupg_process_release (proc); } /* Launch the gpg-agent or the dirmngr if not already running. */ gpg_error_t gc_component_launch (int component) { gpg_error_t err; const char *pgmname; const char *argv[6]; int i; - pid_t pid; + gnupg_process_t proc = NULL; if (component < 0) { err = gc_component_launch (GC_COMPONENT_GPG_AGENT); if (!err) err = gc_component_launch (GC_COMPONENT_KEYBOXD); if (!err) err = gc_component_launch (GC_COMPONENT_DIRMNGR); return err; } if (!(component == GC_COMPONENT_GPG_AGENT || component == GC_COMPONENT_KEYBOXD || component == GC_COMPONENT_DIRMNGR)) { log_error ("%s\n", _("Component not suitable for launching")); gpgconf_failure (0); } if (gc_component_check_options (component, NULL, NULL)) { log_error (_("Configuration file of component %s is broken\n"), gc_component[component].name); if (!opt.quiet) log_info (_("Note: Use the command \"%s%s\" to get details.\n"), gc_component[component].program ? gc_component[component].program : gc_component[component].name, " --gpgconf-test"); gpgconf_failure (0); } pgmname = gnupg_module_name (GNUPG_MODULE_NAME_CONNECT_AGENT); i = 0; if (!gnupg_default_homedir_p ()) { argv[i++] = "--homedir"; argv[i++] = gnupg_homedir (); } if (component == GC_COMPONENT_DIRMNGR) argv[i++] = "--dirmngr"; else if (component == GC_COMPONENT_KEYBOXD) argv[i++] = "--keyboxd"; argv[i++] = "NOP"; argv[i] = NULL; log_assert (i < DIM(argv)); - err = gnupg_spawn_process_fd (pgmname, argv, -1, -1, -1, &pid); + err = gnupg_process_spawn (pgmname, argv, 0, NULL, NULL, &proc); if (!err) - err = gnupg_wait_process (pgmname, pid, 1, NULL); + err = gnupg_process_wait (proc, 1); if (err) gc_error (0, 0, "error running '%s%s%s': %s", pgmname, component == GC_COMPONENT_DIRMNGR? " --dirmngr" : component == GC_COMPONENT_KEYBOXD? " --keyboxd":"", " NOP", gpg_strerror (err)); - gnupg_release_process (pid); + gnupg_process_release (proc); return err; } static void do_runtime_change (int component, int killflag) { int runtime[GC_COMPONENT_NR] = { 0 }; if (component < 0) { for (component = 0; component < GC_COMPONENT_NR; component++) runtime [component] = 1; } else { log_assert (component >= 0 && component < GC_COMPONENT_NR); runtime [component] = 1; } /* Do the restart for the selected components. */ for (component = GC_COMPONENT_NR-1; component >= 0; component--) { if (runtime[component] && gc_component[component].runtime_change) (*gc_component[component].runtime_change) (killflag); } } /* Unconditionally restart COMPONENT. */ void gc_component_kill (int component) { do_runtime_change (component, 1); } /* Unconditionally reload COMPONENT or all components if COMPONENT is -1. */ void gc_component_reload (int component) { do_runtime_change (component, 0); } /* More or less Robust version of dgettext. It has the side effect of switching the codeset to utf-8 because this is what we want to output. In theory it is possible to keep the original code set and switch back for regular diagnostic output (redefine "_(" for that) but given the nature of this tool, being something invoked from other programs, it does not make much sense. */ static const char * my_dgettext (const char *domain, const char *msgid) { if (!msgid || !*msgid) return msgid; /* Shortcut form "" which has the PO files meta data. */ #ifdef USE_SIMPLE_GETTEXT if (domain) { static int switched_codeset; char *text; if (!switched_codeset) { switched_codeset = 1; gettext_use_utf8 (1); } if (!strcmp (domain, "gnupg")) domain = PACKAGE_GT; /* FIXME: we have no dgettext, thus we can't switch. */ text = (char*)gettext (msgid); return text ? text : msgid; } else return msgid; #elif defined(ENABLE_NLS) if (domain) { static int switched_codeset; char *text; if (!switched_codeset) { switched_codeset = 1; bind_textdomain_codeset (PACKAGE_GT, "utf-8"); bindtextdomain (DIRMNGR_NAME, gnupg_localedir ()); bind_textdomain_codeset (DIRMNGR_NAME, "utf-8"); } /* Note: This is a hack to actually use the gnupg2 domain as long we are in a transition phase where gnupg 1.x and 1.9 may coexist. */ if (!strcmp (domain, "gnupg")) domain = PACKAGE_GT; text = dgettext (domain, msgid); return text ? text : msgid; } else return msgid; #else (void)domain; return msgid; #endif } /* Percent-Escape special characters. The string is valid until the next invocation of the function. */ char * gc_percent_escape (const char *src) { static char *esc_str; static int esc_str_len; int new_len = 3 * strlen (src) + 1; char *dst; if (esc_str_len < new_len) { char *new_esc_str = xrealloc (esc_str, new_len); esc_str = new_esc_str; esc_str_len = new_len; } dst = esc_str; while (*src) { if (*src == '%') { *(dst++) = '%'; *(dst++) = '2'; *(dst++) = '5'; } else if (*src == ':') { /* The colon is used as field separator. */ *(dst++) = '%'; *(dst++) = '3'; *(dst++) = 'a'; } else if (*src == ',') { /* The comma is used as list separator. */ *(dst++) = '%'; *(dst++) = '2'; *(dst++) = 'c'; } else if (*src == '\n') { /* The newline is problematic in a line-based format. */ *(dst++) = '%'; *(dst++) = '0'; *(dst++) = 'a'; } else *(dst++) = *(src); src++; } *dst = '\0'; return esc_str; } /* Percent-Deescape special characters. The string is valid until the next invocation of the function. */ static char * percent_deescape (const char *src) { static char *str; static int str_len; int new_len = 3 * strlen (src) + 1; char *dst; if (str_len < new_len) { char *new_str = xrealloc (str, new_len); str = new_str; str_len = new_len; } dst = str; while (*src) { if (*src == '%') { int val = hextobyte (src + 1); if (val < 0) gc_error (1, 0, "malformed end of string %s", src); *(dst++) = (char) val; src += 3; } else *(dst++) = *(src++); } *dst = '\0'; return str; } /* List all components that are available. */ void gc_component_list_components (estream_t out) { gc_component_id_t component; const char *desc; const char *pgmname; for (component = 0; component < GC_COMPONENT_NR; component++) { if (!gc_component[component].program) continue; if (gc_component[component].module_name) pgmname = gnupg_module_name (gc_component[component].module_name); else pgmname = ""; desc = gc_component[component].desc; desc = my_dgettext (gc_component[component].desc_domain, desc); es_fprintf (out, "%s:%s:", gc_component[component].program, gc_percent_escape (desc)); es_fprintf (out, "%s\n", gc_percent_escape (pgmname)); } } static int all_digits_p (const char *p, size_t len) { if (!len) return 0; /* No. */ for (; len; len--, p++) if (!isascii (*p) || !isdigit (*p)) return 0; /* No. */ return 1; /* Yes. */ } /* Collect all error lines from stream FP. Only lines prefixed with TAG are considered. Returns a list of error line items (which may be empty). There is no error return. */ static error_line_t collect_error_output (estream_t fp, const char *tag) { char buffer[1024]; char *p, *p2, *p3; int c, cont_line; unsigned int pos; error_line_t eitem, errlines, *errlines_tail; size_t taglen = strlen (tag); errlines = NULL; errlines_tail = &errlines; pos = 0; cont_line = 0; while ((c=es_getc (fp)) != EOF) { buffer[pos++] = c; if (pos >= sizeof buffer - 5 || c == '\n') { buffer[pos - (c == '\n')] = 0; if (cont_line) ; /*Ignore continuations of previous line. */ else if (!strncmp (buffer, tag, taglen) && buffer[taglen] == ':') { /* "gpgsm: foo:4: bla" */ /* Yep, we are interested in this line. */ p = buffer + taglen + 1; while (*p == ' ' || *p == '\t') p++; trim_trailing_spaces (p); /* Get rid of extra CRs. */ if (!*p) ; /* Empty lines are ignored. */ else if ( (p2 = strchr (p, ':')) && (p3 = strchr (p2+1, ':')) && all_digits_p (p2+1, p3 - (p2+1))) { /* Line in standard compiler format. */ p3++; while (*p3 == ' ' || *p3 == '\t') p3++; eitem = xmalloc (sizeof *eitem + strlen (p)); eitem->next = NULL; strcpy (eitem->buffer, p); eitem->fname = eitem->buffer; eitem->buffer[p2-p] = 0; eitem->errtext = eitem->buffer + (p3 - p); /* (we already checked that there are only ascii digits followed by a colon) */ eitem->lineno = 0; for (p2++; isdigit (*p2); p2++) eitem->lineno = eitem->lineno*10 + (*p2 - '0'); *errlines_tail = eitem; errlines_tail = &eitem->next; } else { /* Other error output. */ eitem = xmalloc (sizeof *eitem + strlen (p)); eitem->next = NULL; strcpy (eitem->buffer, p); eitem->fname = NULL; eitem->errtext = eitem->buffer; eitem->lineno = 0; *errlines_tail = eitem; errlines_tail = &eitem->next; } } pos = 0; /* If this was not a complete line mark that we are in a continuation. */ cont_line = (c != '\n'); } } /* We ignore error lines not terminated by a LF. */ return errlines; } /* Check the options of a single component. If CONF_FILE is NULL the * standard config file is used. If OUT is not NULL the output is * written to that stream. Returns 0 if everything is OK. */ int gc_component_check_options (int component, estream_t out, const char *conf_file) { gpg_error_t err; unsigned int result; const char *pgmname; const char *argv[6]; int i; - pid_t pid; - int exitcode; + gnupg_process_t proc; estream_t errfp; error_line_t errlines; log_assert (component >= 0 && component < GC_COMPONENT_NR); if (!gc_component[component].program) return 0; if (!gc_component[component].module_name) return 0; pgmname = gnupg_module_name (gc_component[component].module_name); i = 0; if (!gnupg_default_homedir_p () && component != GC_COMPONENT_PINENTRY) { argv[i++] = "--homedir"; argv[i++] = gnupg_homedir (); } if (conf_file) { argv[i++] = "--options"; argv[i++] = conf_file; } if (component == GC_COMPONENT_PINENTRY) argv[i++] = "--version"; else argv[i++] = "--gpgconf-test"; argv[i] = NULL; log_assert (i < DIM(argv)); result = 0; errlines = NULL; - err = gnupg_spawn_process (pgmname, argv, NULL, 0, - NULL, NULL, &errfp, &pid); + err = gnupg_process_spawn (pgmname, argv, + GNUPG_PROCESS_STDERR_PIPE, + NULL, NULL, &proc); if (err) result |= 1; /* Program could not be run. */ else { + gnupg_process_get_streams (proc, 0, NULL, NULL, &errfp); errlines = collect_error_output (errfp, gc_component[component].name); - if (gnupg_wait_process (pgmname, pid, 1, &exitcode)) + if (!gnupg_process_wait (proc, 1)) { + int exitcode; + + gnupg_process_ctl (proc, GNUPG_PROCESS_GET_EXIT_ID, &exitcode); if (exitcode == -1) result |= 1; /* Program could not be run or it terminated abnormally. */ - result |= 2; /* Program returned an error. */ + else if (exitcode) + result |= 2; /* Program returned an error. */ } - gnupg_release_process (pid); + gnupg_process_release (proc); es_fclose (errfp); } /* If the program could not be run, we can't tell whether the config file is good. */ if (result & 1) result |= 2; if (out) { const char *desc; error_line_t errptr; desc = gc_component[component].desc; desc = my_dgettext (gc_component[component].desc_domain, desc); es_fprintf (out, "%s:%s:", gc_component[component].program, gc_percent_escape (desc)); es_fputs (gc_percent_escape (pgmname), out); es_fprintf (out, ":%d:%d:", !(result & 1), !(result & 2)); for (errptr = errlines; errptr; errptr = errptr->next) { if (errptr != errlines) es_fputs ("\n:::::", out); /* Continuation line. */ if (errptr->fname) es_fputs (gc_percent_escape (errptr->fname), out); es_putc (':', out); if (errptr->fname) es_fprintf (out, "%u", errptr->lineno); es_putc (':', out); es_fputs (gc_percent_escape (errptr->errtext), out); es_putc (':', out); } es_putc ('\n', out); } while (errlines) { error_line_t tmp = errlines->next; xfree (errlines); errlines = tmp; } return result; } /* Check all components that are available. */ void gc_check_programs (estream_t out) { gc_component_id_t component; for (component = 0; component < GC_COMPONENT_NR; component++) gc_component_check_options (component, out, NULL); } /* Find the component with the name NAME. Returns -1 if not found. */ int gc_component_find (const char *name) { gc_component_id_t idx; for (idx = 0; idx < GC_COMPONENT_NR; idx++) { if (gc_component[idx].program && !strcmp (name, gc_component[idx].program)) return idx; } return -1; } /* List the option OPTION. */ static void list_one_option (gc_component_id_t component, const gc_option_t *option, estream_t out) { const char *desc = NULL; char *arg_name = NULL; unsigned long flags; const char *desc_domain = gc_component[component].desc_domain; /* Don't show options with the ignore attribute. */ if (option->attr_ignore && !option->attr_force) return; if (option->desc) { desc = my_dgettext (desc_domain, option->desc); if (*desc == '|') { const char *arg_tail = strchr (&desc[1], '|'); if (arg_tail) { int arg_len = arg_tail - &desc[1]; arg_name = xmalloc (arg_len + 1); memcpy (arg_name, &desc[1], arg_len); arg_name[arg_len] = '\0'; desc = arg_tail + 1; } } } /* YOU MUST NOT REORDER THE FIELDS IN THIS OUTPUT, AS THEIR ORDER IS PART OF THE EXTERNAL INTERFACE. YOU MUST NOT REMOVE ANY FIELDS. */ /* The name field. */ es_fprintf (out, "%s", option->name); /* The flags field. */ flags = 0; if (option->is_header) flags |= GC_OPT_FLAG_GROUP; if (option->is_list) flags |= GC_OPT_FLAG_LIST; if (option->runtime) flags |= GC_OPT_FLAG_RUNTIME; if (option->has_default) flags |= GC_OPT_FLAG_DEFAULT; if (option->def_in_desc) flags |= GC_OPT_FLAG_DEF_DESC; if (option->no_arg_desc) flags |= GC_OPT_FLAG_NO_ARG_DESC; if (option->no_change) flags |= GC_OPT_FLAG_NO_CHANGE; if (option->attr_force) flags |= GC_OPT_FLAG_NO_CHANGE; es_fprintf (out, ":%lu", flags); if (opt.verbose) { es_putc (' ', out); if (!flags) es_fprintf (out, "none"); else { unsigned long flag = 0; unsigned long first = 1; while (flags) { if (flags & 1) { if (first) first = 0; else es_putc (',', out); es_fprintf (out, "%s", gc_flag[flag].name); } flags >>= 1; flag++; } } } /* The level field. */ es_fprintf (out, ":%u", option->level); if (opt.verbose) es_fprintf (out, " %s", gc_level[option->level].name); /* The description field. */ es_fprintf (out, ":%s", desc ? gc_percent_escape (desc) : ""); /* The type field. */ es_fprintf (out, ":%u", option->arg_type); if (opt.verbose) es_fprintf (out, " %s", gc_arg_type[option->arg_type].name); /* The alternate type field. */ es_fprintf (out, ":%u", gc_arg_type[option->arg_type].fallback); if (opt.verbose) es_fprintf (out, " %s", gc_arg_type[gc_arg_type[option->arg_type].fallback].name); /* The argument name field. */ es_fprintf (out, ":%s", arg_name ? gc_percent_escape (arg_name) : ""); xfree (arg_name); /* The default value field. */ es_fprintf (out, ":%s", option->default_value ? option->default_value : ""); /* The default argument field. This was never used and is thus empty. */ es_fprintf (out, ":"); /* The value field. */ if (gc_arg_type[option->arg_type].fallback == GC_ARG_TYPE_NONE && option->is_list && option->value) { /* The special format "1,1,1,1,...,1" is converted to a number here. */ es_fprintf (out, ":%u", (unsigned int)((strlen (option->value) + 1) / 2)); } else es_fprintf (out, ":%s", option->value ? option->value : ""); /* ADD NEW FIELDS HERE. */ es_putc ('\n', out); } /* List all options of the component COMPONENT. */ void gc_component_list_options (int component, estream_t out) { const gc_option_t *option = gc_component[component].options; for ( ; option && option->name; option++) { /* Do not output unknown or internal options. */ if (!option->is_header && option->level == GC_LEVEL_INTERNAL) continue; if (option->is_header) { const gc_option_t *group_option = option + 1; gc_expert_level_t level = GC_LEVEL_NR; /* The manual states that the group level is always the minimum of the levels of all contained options. Due to different active options, and because it is hard to maintain manually, we calculate it here. The value in the global static table is ignored. */ for ( ; group_option->name; group_option++) { if (group_option->is_header) break; if (group_option->level < level) level = group_option->level; } /* Check if group is empty. */ if (level != GC_LEVEL_NR) { gc_option_t opt_copy; /* Fix up the group level. */ opt_copy = *option; opt_copy.level = level; list_one_option (component, &opt_copy, out); } } else list_one_option (component, option, out); } } /* Return true if the option NAME is known and that we want it as * gpgconf managed option. */ static known_option_t * is_known_option (gc_component_id_t component, const char *name) { known_option_t *option = gc_component[component].known_options; if (option) { for (; option->name; option++) if (!strcmp (option->name, name)) break; } return (option && option->name)? option : NULL; } /* Find the option NAME in component COMPONENT. Returns pointer to * the option descriptor or NULL if not found. */ static gc_option_t * find_option (gc_component_id_t component, const char *name) { gc_option_t *option = gc_component[component].options; if (option) { for (; option->name; option++) { if (!option->is_header && !strcmp (option->name, name)) return option; } } return NULL; } struct read_line_wrapper_parm_s { const char *pgmname; estream_t fp; char *line; size_t line_len; const char **extra_lines; int extra_lines_idx; char *extra_line_buffer; }; /* Helper for retrieve_options_from_program. */ static ssize_t read_line_wrapper (struct read_line_wrapper_parm_s *parm) { ssize_t length; const char *extra_line; if (parm->fp) { length = es_read_line (parm->fp, &parm->line, &parm->line_len, NULL); if (length > 0) return length; if (length < 0 || es_ferror (parm->fp)) gc_error (1, errno, "error reading from %s", parm->pgmname); if (es_fclose (parm->fp)) gc_error (1, errno, "error closing %s", parm->pgmname); /* EOF seen. */ parm->fp = NULL; } /* Return the made up lines. */ if (!parm->extra_lines || !(extra_line = parm->extra_lines[parm->extra_lines_idx])) return -1; /* This is really the EOF. */ parm->extra_lines_idx++; xfree (parm->extra_line_buffer); parm->extra_line_buffer = xstrdup (extra_line); return strlen (parm->extra_line_buffer); } /* Retrieve the options for the component COMPONENT. With * ONLY_INSTALLED set components which are not installed are silently * ignored. */ static void retrieve_options_from_program (gc_component_id_t component, int only_installed) { gpg_error_t err; const char *pgmname; const char *argv[2]; estream_t outfp; - int exitcode; - pid_t pid; + gnupg_process_t proc; known_option_t *known_option; gc_option_t *option; char *line = NULL; size_t line_len; ssize_t length; const char *config_name; gpgrt_argparse_t pargs; int dummy_argc; char *twopartconfig_name = NULL; gpgrt_opt_t *opt_table = NULL; /* A malloced option table. */ size_t opt_table_used = 0; /* Its current length. */ size_t opt_table_size = 0; /* Its allocated length. */ gc_option_t *opt_info = NULL; /* A malloced options table. */ size_t opt_info_used = 0; /* Its current length. */ size_t opt_info_size = 0; /* Its allocated length. */ int i; struct read_line_wrapper_parm_s read_line_parm; int pseudo_count; pgmname = (gc_component[component].module_name ? gnupg_module_name (gc_component[component].module_name) : gc_component[component].program ); if (only_installed && gnupg_access (pgmname, X_OK)) { return; /* The component is not installed. */ } /* First we need to read the option table from the program. */ argv[0] = "--dump-option-table"; argv[1] = NULL; - err = gnupg_spawn_process (pgmname, argv, NULL, 0, - NULL, &outfp, NULL, &pid); + err = gnupg_process_spawn (pgmname, argv, + GNUPG_PROCESS_STDOUT_PIPE, + NULL, NULL, &proc); if (err) { gc_error (1, 0, "could not gather option table from '%s': %s", pgmname, gpg_strerror (err)); } + gnupg_process_get_streams (proc, 0, NULL, &outfp, NULL); + read_line_parm.pgmname = pgmname; read_line_parm.fp = outfp; read_line_parm.line = line; read_line_parm.line_len = line_len = 0; read_line_parm.extra_line_buffer = NULL; read_line_parm.extra_lines = gc_component[component].known_pseudo_options; read_line_parm.extra_lines_idx = 0; pseudo_count = 0; while ((length = read_line_wrapper (&read_line_parm)) > 0) { const char *fields[4]; const char *optname, *optdesc; unsigned int optflags; int short_opt; gc_arg_type_t arg_type; int pseudo = 0; if (read_line_parm.extra_line_buffer) { line = read_line_parm.extra_line_buffer; pseudo = 1; pseudo_count++; } else line = read_line_parm.line; /* Strip newline and carriage return, if present. */ while (length > 0 && (line[length - 1] == '\n' || line[length - 1] == '\r')) line[--length] = '\0'; if (split_fields_colon (line, fields, DIM (fields)) < 4) { gc_error (0,0, "WARNING: invalid line in option table of '%s'\n", pgmname); continue; } optname = fields[0]; short_opt = atoi (fields[1]); if (short_opt < 1 && !pseudo) { gc_error (0,0, "WARNING: bad short option in option table of '%s'\n", pgmname); continue; } optflags = strtoul (fields[2], NULL, 10); if ((optflags & ARGPARSE_OPT_HEADER)) known_option = NULL; /* We want all header-only options. */ else if ((known_option = is_known_option (component, optname))) ; /* Yes we want this one. */ else continue; /* No need to store this option description. */ /* The +1 here is to make sure that we will have a zero item at * the end of the table. */ if (opt_table_used + 1 >= opt_table_size) { /* Note that this also does the initial allocation. */ opt_table_size += 128; opt_table = xreallocarray (opt_table, opt_table_used, opt_table_size, sizeof *opt_table); } /* The +1 here is to make sure that we will have a zero item at * the end of the table. */ if (opt_info_used + 1 >= opt_info_size) { /* Note that this also does the initial allocation. */ opt_info_size += 128; opt_info = xreallocarray (opt_info, opt_info_used, opt_info_size, sizeof *opt_info); } /* The +1 here accounts for the two items we are going to add to * the global string table. */ if (string_array_used + 1 >= string_array_size) { string_array_size += 256; string_array = xreallocarray (string_array, string_array_used, string_array_size, sizeof *string_array); } optname = string_array[string_array_used++] = xstrdup (fields[0]); optdesc = string_array[string_array_used++] = xstrdup (fields[3]); /* Create an option table which can then be supplied to * gpgrt_parser. Unfortunately there is no private pointer in * the public option table struct so that we can't add extra * data we need here. Thus we need to build up another table * for such info and for ease of use we also copy the tehre the * data from the option table. It is not possible to use the * known_option_s for this because that one does not carry * header lines and it might also be problematic to use such * static tables for caching options and default values. */ if (!pseudo) { opt_table[opt_table_used].long_opt = optname; opt_table[opt_table_used].short_opt = short_opt; opt_table[opt_table_used].description = optdesc; opt_table[opt_table_used].flags = optflags; opt_table_used++; } /* Note that as per argparser specs the opt_table uses "@" to * specifify an empty description. In the DESC script of * options (opt_info_t) we want to have a real empty string. */ opt_info[opt_info_used].name = optname; if (*optdesc == '@' && !optdesc[1]) opt_info[opt_info_used].desc = optdesc+1; else opt_info[opt_info_used].desc = optdesc; /* Unfortunately we need to remap the types. */ switch ((optflags & ARGPARSE_TYPE_MASK)) { case ARGPARSE_TYPE_INT: arg_type = GC_ARG_TYPE_INT32; break; case ARGPARSE_TYPE_LONG: arg_type = GC_ARG_TYPE_INT32; break; case ARGPARSE_TYPE_ULONG: arg_type = GC_ARG_TYPE_UINT32; break; case ARGPARSE_TYPE_STRING: arg_type = GC_ARG_TYPE_STRING; break; default: arg_type = GC_ARG_TYPE_NONE; break; } opt_info[opt_info_used].arg_type = arg_type; if (pseudo) /* Pseudo options are always no_change. */ opt_info[opt_info_used].no_change = 1; if ((optflags & ARGPARSE_OPT_HEADER)) opt_info[opt_info_used].is_header = 1; if (known_option) { if ((known_option->flags & GC_OPT_FLAG_LIST)) opt_info[opt_info_used].is_list = 1; /* FIXME: The next can also be taken from opt_table->flags. * We need to check the code whether both specifications match. */ if ((known_option->flags & GC_OPT_FLAG_ARG_OPT)) opt_info[opt_info_used].opt_arg = 1; if ((known_option->flags & GC_OPT_FLAG_RUNTIME)) opt_info[opt_info_used].runtime = 1; opt_info[opt_info_used].level = known_option->level; /* Override the received argtype by a complex type. */ if (known_option->arg_type) opt_info[opt_info_used].arg_type = known_option->arg_type; } opt_info_used++; } xfree (read_line_parm.extra_line_buffer); line = read_line_parm.line; line_len = read_line_parm.line_len; log_assert (opt_table_used + pseudo_count == opt_info_used); + err = gnupg_process_wait (proc, 1); + if (!err) + { + int exitcode; - err = gnupg_wait_process (pgmname, pid, 1, &exitcode); - if (err) - gc_error (1, 0, "running %s failed (exitcode=%d): %s", - pgmname, exitcode, gpg_strerror (err)); - gnupg_release_process (pid); + gnupg_process_ctl (proc, GNUPG_PROCESS_GET_EXIT_ID, &exitcode); + if (exitcode) + gc_error (1, 0, "running %s failed (exitcode=%d): %s", + pgmname, exitcode, gpg_strerror (err)); + } + gnupg_process_release (proc); /* Make the gpgrt option table and the internal option table available. */ gc_component[component].opt_table = opt_table; gc_component[component].options = opt_info; /* Now read the default options. */ argv[0] = "--gpgconf-list"; argv[1] = NULL; - err = gnupg_spawn_process (pgmname, argv, NULL, 0, - NULL, &outfp, NULL, &pid); + err = gnupg_process_spawn (pgmname, argv, + GNUPG_PROCESS_STDOUT_PIPE, + NULL, NULL, &proc); if (err) { gc_error (1, 0, "could not gather active options from '%s': %s", pgmname, gpg_strerror (err)); } + gnupg_process_get_streams (proc, 0, NULL, &outfp, NULL); + while ((length = es_read_line (outfp, &line, &line_len, NULL)) > 0) { char *linep; unsigned long flags = 0; char *default_value = NULL; /* Strip newline and carriage return, if present. */ while (length > 0 && (line[length - 1] == '\n' || line[length - 1] == '\r')) line[--length] = '\0'; linep = strchr (line, ':'); if (linep) *(linep++) = '\0'; /* Extract additional flags. Default to none. */ if (linep) { char *end; char *tail; end = strchr (linep, ':'); if (end) *(end++) = '\0'; gpg_err_set_errno (0); flags = strtoul (linep, &tail, 0); if (errno) gc_error (1, errno, "malformed flags in option %s from %s", line, pgmname); if (!(*tail == '\0' || *tail == ':' || *tail == ' ')) gc_error (1, 0, "garbage after flags in option %s from %s", line, pgmname); linep = end; } /* Extract default value, if present. Default to empty if not. */ if (linep) { char *end; end = strchr (linep, ':'); if (end) *(end++) = '\0'; if ((flags & GC_OPT_FLAG_DEFAULT)) default_value = linep; linep = end; } /* Look up the option in the component and install the configuration data. */ option = find_option (component, line); if (option) { if (option->gpgconf_list) gc_error (1, errno, "option %s returned twice from \"%s --gpgconf-list\"", line, pgmname); option->gpgconf_list = 1; if ((flags & GC_OPT_FLAG_DEFAULT)) option->has_default = 1; if ((flags & GC_OPT_FLAG_DEF_DESC)) option->def_in_desc = 1; if ((flags & GC_OPT_FLAG_NO_ARG_DESC)) option->no_arg_desc = 1; if ((flags & GC_OPT_FLAG_NO_CHANGE)) option->no_change = 1; if (default_value && *default_value) option->default_value = xstrdup (default_value); } } if (length < 0 || es_ferror (outfp)) gc_error (1, errno, "error reading from %s", pgmname); if (es_fclose (outfp)) gc_error (1, errno, "error closing %s", pgmname); - err = gnupg_wait_process (pgmname, pid, 1, &exitcode); - if (err) - gc_error (1, 0, "running %s failed (exitcode=%d): %s", - pgmname, exitcode, gpg_strerror (err)); - gnupg_release_process (pid); + err = gnupg_process_wait (proc, 1); + if (!err) + { + int exitcode; + + gnupg_process_ctl (proc, GNUPG_PROCESS_GET_EXIT_ID, &exitcode); + if (exitcode) + gc_error (1, 0, "running %s failed (exitcode=%d): %s", + pgmname, exitcode, gpg_strerror (err)); + } + gnupg_process_release (proc); /* At this point, we can parse the configuration file. */ config_name = gc_component[component].option_config_filename; if (!config_name) gc_error (1, 0, "name of config file for %s is not known\n", pgmname); if (!gnupg_default_homedir_p ()) { /* This is not the default homedir. We need to take an absolute * config name for the user config file; gpgrt_argparser * fortunately supports this. */ char *tmp = make_filename (gnupg_homedir (), config_name, NULL); twopartconfig_name = xstrconcat (config_name, PATHSEP_S, tmp, NULL); xfree (tmp); config_name = twopartconfig_name; } memset (&pargs, 0, sizeof pargs); dummy_argc = 0; pargs.argc = &dummy_argc; pargs.flags = (ARGPARSE_FLAG_KEEP | ARGPARSE_FLAG_SYS | ARGPARSE_FLAG_USER | ARGPARSE_FLAG_WITHATTR); if (opt.verbose) pargs.flags |= ARGPARSE_FLAG_VERBOSE; while (gpgrt_argparser (&pargs, opt_table, config_name)) { char *opt_value; if (pargs.r_opt == ARGPARSE_CONFFILE) { /* log_debug ("current conffile='%s'\n", */ /* pargs.r_type? pargs.r.ret_str: "[cmdline]"); */ continue; } if ((pargs.r_type & ARGPARSE_OPT_IGNORE)) continue; /* We only have the short option. Search in the option table * for the long option name. */ for (i=0; opt_table[i].short_opt; i++) if (opt_table[i].short_opt == pargs.r_opt) break; if (!opt_table[i].short_opt || !opt_table[i].long_opt) continue; /* No or only a short option - ignore. */ /* Look up the option from the config file in our list of * supported options. */ option= find_option (component, opt_table[i].long_opt); if (!option) continue; /* We don't want to handle this option. */ /* Set the force and ignore attributes. The idea is that there * is no way to clear them again, thus we set them when first * encountered. */ if ((pargs.r_type & ARGPARSE_ATTR_FORCE)) option->attr_force = 1; if ((pargs.r_type & ARGPARSE_ATTR_IGNORE)) option->attr_ignore = 1; /* If an option has been ignored, there is no need to return * that option with gpgconf --list-options. */ if (option->attr_ignore) continue; switch ((pargs.r_type & ARGPARSE_TYPE_MASK)) { case ARGPARSE_TYPE_INT: opt_value = xasprintf ("%d", pargs.r.ret_int); break; case ARGPARSE_TYPE_LONG: opt_value = xasprintf ("%ld", pargs.r.ret_long); break; case ARGPARSE_TYPE_ULONG: opt_value = xasprintf ("%lu", pargs.r.ret_ulong); break; case ARGPARSE_TYPE_STRING: if (!pargs.r.ret_str) opt_value = xstrdup ("\"(none)"); /* We should not see this. */ else opt_value = xasprintf ("\"%s", gc_percent_escape (pargs.r.ret_str)); break; default: /* ARGPARSE_TYPE_NONE or any unknown type. */ opt_value = xstrdup ("1"); /* Make sure we have some value. */ break; } /* Now enter the value read from the config file into the table. */ if (!option->is_list) { xfree (option->value); option->value = opt_value; } else if (!option->value) /* LIST but first item. */ option->value = opt_value; else { char *old = option->value; option->value = xstrconcat (old, ",", opt_value, NULL); xfree (old); xfree (opt_value); } } xfree (line); xfree (twopartconfig_name); } /* Retrieve the currently active options and their defaults for this component. Using -1 for component will retrieve all options from all installed components. */ void gc_component_retrieve_options (int component) { int process_all = 0; if (component == -1) { process_all = 1; component = 0; } do { if (component == GC_COMPONENT_PINENTRY) continue; /* Skip this dummy component. */ if (gc_component[component].program) retrieve_options_from_program (component, process_all); } while (process_all && ++component < GC_COMPONENT_NR); } /* Perform a simple validity check based on the type. Return in * NEW_VALUE_NR the value of the number in NEW_VALUE if OPTION is of * type GC_ARG_TYPE_NONE. If VERBATIM is set the profile parsing mode * is used. */ static void option_check_validity (gc_component_id_t component, gc_option_t *option, unsigned long flags, char *new_value, unsigned long *new_value_nr, int verbatim) { char *arg; (void)component; if (option->new_flags || option->new_value) gc_error (1, 0, "option %s already changed", option->name); if (flags & GC_OPT_FLAG_DEFAULT) { if (*new_value) gc_error (1, 0, "argument %s provided for deleted option %s", new_value, option->name); return; } /* GC_ARG_TYPE_NONE options have special list treatment. */ if (gc_arg_type[option->arg_type].fallback == GC_ARG_TYPE_NONE) { char *tail; gpg_err_set_errno (0); *new_value_nr = strtoul (new_value, &tail, 0); if (errno) gc_error (1, errno, "invalid argument for option %s", option->name); if (*tail) gc_error (1, 0, "garbage after argument for option %s", option->name); if (!option->is_list) { if (*new_value_nr != 1) gc_error (1, 0, "argument for non-list option %s of type 0 " "(none) must be 1", option->name); } else { if (*new_value_nr == 0) gc_error (1, 0, "argument for option %s of type 0 (none) " "must be positive", option->name); } return; } arg = new_value; do { if (*arg == '\0' || (*arg == ',' && !verbatim)) { if (!option->opt_arg) gc_error (1, 0, "argument required for option %s", option->name); if (*arg == ',' && !verbatim && !option->is_list) gc_error (1, 0, "list found for non-list option %s", option->name); } else if (gc_arg_type[option->arg_type].fallback == GC_ARG_TYPE_STRING) { if (*arg != '"' && !verbatim) gc_error (1, 0, "string argument for option %s must begin " "with a quote (\") character", option->name); /* FIXME: We do not allow empty string arguments for now, as we do not quote arguments in configuration files, and thus no argument is indistinguishable from the empty string. */ if (arg[1] == '\0' || (arg[1] == ',' && !verbatim)) gc_error (1, 0, "empty string argument for option %s is " "currently not allowed. Please report this!", option->name); } else if (gc_arg_type[option->arg_type].fallback == GC_ARG_TYPE_INT32) { long res; gpg_err_set_errno (0); res = strtol (arg, &arg, 0); (void) res; if (errno) gc_error (1, errno, "invalid argument for option %s", option->name); if (*arg != '\0' && (*arg != ',' || verbatim)) gc_error (1, 0, "garbage after argument for option %s", option->name); } else if (gc_arg_type[option->arg_type].fallback == GC_ARG_TYPE_UINT32) { unsigned long res; gpg_err_set_errno (0); res = strtoul (arg, &arg, 0); (void) res; if (errno) gc_error (1, errno, "invalid argument for option %s", option->name); if (*arg != '\0' && (*arg != ',' || verbatim)) gc_error (1, 0, "garbage after argument for option %s", option->name); } arg = verbatim? strchr (arg, ',') : NULL; if (arg) arg++; } while (arg && *arg); } #ifdef HAVE_W32_SYSTEM int copy_file (const char *src_name, const char *dst_name) { #define BUF_LEN 4096 char buffer[BUF_LEN]; int len; gpgrt_stream_t src; gpgrt_stream_t dst; src = gpgrt_fopen (src_name, "r"); if (src == NULL) return -1; dst = gpgrt_fopen (dst_name, "w"); if (dst == NULL) { int saved_err = errno; gpgrt_fclose (src); gpg_err_set_errno (saved_err); return -1; } do { int written; len = gpgrt_fread (buffer, 1, BUF_LEN, src); if (len == 0) break; written = gpgrt_fwrite (buffer, 1, len, dst); if (written != len) break; } while (! gpgrt_feof (src) && ! gpgrt_ferror (src) && ! gpgrt_ferror (dst)); if (gpgrt_ferror (src) || gpgrt_ferror (dst) || ! gpgrt_feof (src)) { int saved_errno = errno; gpgrt_fclose (src); gpgrt_fclose (dst); unlink (dst_name); gpg_err_set_errno (saved_errno); return -1; } if (gpgrt_fclose (dst)) gc_error (1, errno, "error closing %s", dst_name); if (gpgrt_fclose (src)) gc_error (1, errno, "error closing %s", src_name); return 0; } #endif /* HAVE_W32_SYSTEM */ /* Create and verify the new configuration file for the specified * component. Returns 0 on success and -1 on error. If * VERBATIM is set the profile mode is used. This function may store * pointers to malloced strings in SRC_FILENAMEP, DEST_FILENAMEP, and * ORIG_FILENAMEP. Those must be freed by the caller. The strings * refer to three versions of the configuration file: * * SRC_FILENAME: The updated configuration is written to this file. * DEST_FILENAME: Name of the configuration file read by the * component. * ORIG_FILENAME: A backup of the previous configuration file. * * To apply the configuration change, rename SRC_FILENAME to * DEST_FILENAME. To revert to the previous configuration, rename * ORIG_FILENAME to DEST_FILENAME. */ static int change_options_program (gc_component_id_t component, char **src_filenamep, char **dest_filenamep, char **orig_filenamep, int verbatim) { static const char marker[] = "###+++--- " GPGCONF_DISP_NAME " ---+++###"; /* True if we are within the marker in the config file. */ int in_marker = 0; gc_option_t *option; char *line = NULL; size_t line_len; ssize_t length; int res; int fd; gpgrt_stream_t src_file = NULL; gpgrt_stream_t dest_file = NULL; char *src_filename; char *dest_filename; char *orig_filename; /* Special hack for gpg, see below. */ int utf8strings_seen = 0; /* FIXME. Throughout the function, do better error reporting. */ if (!gc_component[component].option_config_filename) gc_error (1, 0, "name of config file for %s is not known\n", gc_component[component].name); dest_filename = make_absfilename (gnupg_homedir (), gc_component[component].option_config_filename, NULL); src_filename = xasprintf ("%s.%s.%i.new", dest_filename, GPGCONF_NAME, (int)getpid ()); orig_filename = xasprintf ("%s.%s.%i.bak", dest_filename, GPGCONF_NAME, (int)getpid ()); #ifdef HAVE_W32_SYSTEM res = copy_file (dest_filename, orig_filename); #else res = link (dest_filename, orig_filename); #endif if (res < 0 && errno != ENOENT) { xfree (dest_filename); xfree (src_filename); xfree (orig_filename); return -1; } if (res < 0) { xfree (orig_filename); orig_filename = NULL; } /* We now initialize the return strings, so the caller can do the cleanup for us. */ *src_filenamep = src_filename; *dest_filenamep = dest_filename; *orig_filenamep = orig_filename; /* Use open() so that we can use O_EXCL. * FIXME: gpgrt has an x flag for quite some time now - use that. */ fd = gnupg_open (src_filename, O_CREAT | O_EXCL | O_WRONLY, 0644); if (fd < 0) return -1; src_file = gpgrt_fdopen (fd, "w"); res = errno; if (!src_file) { gpg_err_set_errno (res); return -1; } /* Only if ORIG_FILENAME is not NULL did the configuration file exist already. In this case, we will copy its content into the new configuration file, changing it to our liking in the process. */ if (orig_filename) { dest_file = gpgrt_fopen (dest_filename, "r"); if (!dest_file) goto change_one_err; while ((length = gpgrt_read_line (dest_file, &line, &line_len, NULL)) > 0) { int disable = 0; char *start; if (!strncmp (marker, line, sizeof (marker) - 1)) { if (!in_marker) in_marker = 1; else break; } else if (component == GC_COMPONENT_GPG && in_marker && ! strcmp ("utf8-strings\n", line)) { /* Strip duplicated entries. */ if (utf8strings_seen) disable = 1; else utf8strings_seen = 1; } start = line; while (*start == ' ' || *start == '\t') start++; if (*start && *start != '\r' && *start != '\n' && *start != '#') { char *end; char saved_end; end = start; while (*end && *end != ' ' && *end != '\t' && *end != '\r' && *end != '\n' && *end != '#') end++; saved_end = *end; *end = '\0'; option = find_option (component, start); *end = saved_end; if (option && ((option->new_flags & GC_OPT_FLAG_DEFAULT) || option->new_value)) disable = 1; } if (disable) { if (!in_marker) { gpgrt_fprintf (src_file, "# %s disabled this option here at %s\n", GPGCONF_DISP_NAME, asctimestamp (gnupg_get_time ())); if (gpgrt_ferror (src_file)) goto change_one_err; gpgrt_fprintf (src_file, "# %s", line); if (gpgrt_ferror (src_file)) goto change_one_err; } } else { gpgrt_fprintf (src_file, "%s", line); if (gpgrt_ferror (src_file)) goto change_one_err; } } if (length < 0 || gpgrt_ferror (dest_file)) goto change_one_err; } if (!in_marker) { /* There was no marker. This is the first time we edit the file. We add our own marker at the end of the file and proceed. Note that we first write a newline, this guards us against files which lack the newline at the end of the last line, while it doesn't hurt us in all other cases. */ gpgrt_fprintf (src_file, "\n%s\n", marker); if (gpgrt_ferror (src_file)) goto change_one_err; } /* At this point, we have copied everything up to the end marker into the new file, except for the options we are going to change. Now, dump the changed options (except for those we are going to revert to their default), and write the end marker, possibly followed by the rest of the original file. */ /* We have to turn on UTF8 strings for GnuPG. */ if (component == GC_COMPONENT_GPG && ! utf8strings_seen) gpgrt_fprintf (src_file, "utf8-strings\n"); option = gc_component[component].options; for ( ; option->name; option++) { if (!option->is_header && option->new_value) { char *arg = option->new_value; do { if (*arg == '\0' || *arg == ',') { gpgrt_fprintf (src_file, "%s\n", option->name); if (gpgrt_ferror (src_file)) goto change_one_err; } else if (gc_arg_type[option->arg_type].fallback == GC_ARG_TYPE_NONE) { log_assert (*arg == '1'); gpgrt_fprintf (src_file, "%s\n", option->name); if (gpgrt_ferror (src_file)) goto change_one_err; arg++; } else if (gc_arg_type[option->arg_type].fallback == GC_ARG_TYPE_STRING) { char *end; if (!verbatim) { log_assert (*arg == '"'); arg++; end = strchr (arg, ','); if (end) *end = '\0'; } else end = NULL; gpgrt_fprintf (src_file, "%s %s\n", option->name, verbatim? arg : percent_deescape (arg)); if (gpgrt_ferror (src_file)) goto change_one_err; if (end) *end = ','; arg = end; } else { char *end; end = strchr (arg, ','); if (end) *end = '\0'; gpgrt_fprintf (src_file, "%s %s\n", option->name, arg); if (gpgrt_ferror (src_file)) goto change_one_err; if (end) *end = ','; arg = end; } log_assert (arg == NULL || *arg == '\0' || *arg == ','); if (arg && *arg == ',') arg++; } while (arg && *arg); } } gpgrt_fprintf (src_file, "%s %s\n", marker, asctimestamp (gnupg_get_time ())); if (gpgrt_ferror (src_file)) goto change_one_err; if (!in_marker) { gpgrt_fprintf (src_file, "# %s edited this configuration file.\n", GPGCONF_DISP_NAME); if (gpgrt_ferror (src_file)) goto change_one_err; gpgrt_fprintf (src_file, "# It will disable options before this marked " "block, but it will\n"); if (gpgrt_ferror (src_file)) goto change_one_err; gpgrt_fprintf (src_file, "# never change anything below these lines.\n"); if (gpgrt_ferror (src_file)) goto change_one_err; } if (dest_file) { while ((length = gpgrt_read_line (dest_file, &line, &line_len, NULL)) > 0) { gpgrt_fprintf (src_file, "%s", line); if (gpgrt_ferror (src_file)) goto change_one_err; } if (length < 0 || gpgrt_ferror (dest_file)) goto change_one_err; } xfree (line); line = NULL; res = gpgrt_fclose (src_file); if (res) { res = errno; close (fd); if (dest_file) gpgrt_fclose (dest_file); gpg_err_set_errno (res); return -1; } close (fd); if (dest_file) { res = gpgrt_fclose (dest_file); if (res) return -1; } return 0; change_one_err: xfree (line); res = errno; if (src_file) { gpgrt_fclose (src_file); close (fd); } if (dest_file) gpgrt_fclose (dest_file); gpg_err_set_errno (res); return -1; } /* Common code for gc_component_change_options and * gc_process_gpgconf_conf. If VERBATIM is set the profile parsing * mode is used. */ static void change_one_value (gc_component_id_t component, gc_option_t *option, int *r_runtime, unsigned long flags, char *new_value, int verbatim) { unsigned long new_value_nr = 0; option_check_validity (component, option, flags, new_value, &new_value_nr, verbatim); if (option->runtime) *r_runtime = 1; option->new_flags = flags; if (!(flags & GC_OPT_FLAG_DEFAULT)) { if (gc_arg_type[option->arg_type].fallback == GC_ARG_TYPE_NONE && option->is_list) { char *str; /* We convert the number to a list of 1's for convenient list handling. */ log_assert (new_value_nr > 0); option->new_value = xmalloc ((2 * (new_value_nr - 1) + 1) + 1); str = option->new_value; *(str++) = '1'; while (--new_value_nr > 0) { *(str++) = ','; *(str++) = '1'; } *(str++) = '\0'; } else option->new_value = xstrdup (new_value); } } /* Read the modifications from IN and apply them. If IN is NULL the modifications are expected to already have been set to the global table. If VERBATIM is set the profile mode is used. */ void gc_component_change_options (int component, estream_t in, estream_t out, int verbatim) { int err = 0; int block = 0; int runtime = 0; char *src_filename = NULL; char *dest_filename = NULL; char *orig_filename = NULL; gc_option_t *option; char *line = NULL; size_t line_len = 0; ssize_t length; if (component == GC_COMPONENT_PINENTRY) return; /* Dummy component for now. */ if (in) { /* Read options from the file IN. */ while ((length = es_read_line (in, &line, &line_len, NULL)) > 0) { char *linep; unsigned long flags = 0; char *new_value = ""; /* Strip newline and carriage return, if present. */ while (length > 0 && (line[length - 1] == '\n' || line[length - 1] == '\r')) line[--length] = '\0'; linep = strchr (line, ':'); if (linep) *(linep++) = '\0'; /* Extract additional flags. Default to none. */ if (linep) { char *end; char *tail; end = strchr (linep, ':'); if (end) *(end++) = '\0'; gpg_err_set_errno (0); flags = strtoul (linep, &tail, 0); if (errno) gc_error (1, errno, "malformed flags in option %s", line); if (!(*tail == '\0' || *tail == ':' || *tail == ' ')) gc_error (1, 0, "garbage after flags in option %s", line); linep = end; } /* Don't allow setting of the no change flag. */ flags &= ~GC_OPT_FLAG_NO_CHANGE; /* Extract default value, if present. Default to empty if not. */ if (linep) { char *end; end = strchr (linep, ':'); if (end) *(end++) = '\0'; new_value = linep; linep = end; } option = find_option (component, line); if (!option) gc_error (1, 0, "unknown option %s", line); if (option->no_change) { gc_error (0, 0, "ignoring new value for option %s", option->name); continue; } change_one_value (component, option, &runtime, flags, new_value, 0); } if (length < 0 || gpgrt_ferror (in)) gc_error (1, errno, "error reading stream 'in'"); } /* Now that we have collected and locally verified the changes, write them out to new configuration files, verify them externally, and then commit them. */ option = gc_component[component].options; while (option && option->name) { /* Go on if there is nothing to do. */ if (src_filename || !(option->new_flags || option->new_value)) { option++; continue; } if (gc_component[component].program) { err = change_options_program (component, &src_filename, &dest_filename, &orig_filename, verbatim); if (! err) { /* External verification. */ err = gc_component_check_options (component, out, src_filename); if (err) { gc_error (0, 0, _("External verification of component %s failed"), gc_component[component].name); gpg_err_set_errno (EINVAL); } } } if (err) break; option++; } /* We are trying to atomically commit all changes. Unfortunately, we cannot rely on gnupg_rename_file to manage the signals for us, doing so would require us to pass NULL as BLOCK to any subsequent call to it. Instead, we just manage the signal handling manually. */ block = 1; gnupg_block_all_signals (); if (!err && !opt.dry_run) { if (src_filename) { /* FIXME: Make a verification here. */ log_assert (dest_filename); if (orig_filename) err = gnupg_rename_file (src_filename, dest_filename, NULL); else { #ifdef HAVE_W32_SYSTEM /* We skip the unlink if we expect the file not to be * there. */ err = gnupg_rename_file (src_filename, dest_filename, NULL); #else /* HAVE_W32_SYSTEM */ /* This is a bit safer than rename() because we expect * DEST_FILENAME not to be there. If it happens to be * there, this will fail. */ err = link (src_filename, dest_filename); if (!err) err = unlink (src_filename); #endif /* !HAVE_W32_SYSTEM */ } if (!err) { xfree (src_filename); src_filename = NULL; } } } if (err || opt.dry_run) { int saved_errno = errno; /* An error occurred or a dry-run is requested. */ if (src_filename) { /* The change was not yet committed. */ unlink (src_filename); if (orig_filename) unlink (orig_filename); } else { /* The changes were already committed. FIXME: This is a tad dangerous, as we don't know if we don't overwrite a version of the file that is even newer than the one we just installed. */ if (orig_filename) gnupg_rename_file (orig_filename, dest_filename, NULL); else unlink (dest_filename); } if (err) gc_error (1, saved_errno, "could not commit changes"); /* Fall-through for dry run. */ goto leave; } /* If it all worked, notify the daemons of the changes. */ if (opt.runtime) do_runtime_change (component, 0); /* Move the per-process backup file into its place. */ if (orig_filename) { char *backup_filename; log_assert (dest_filename); backup_filename = xasprintf ("%s.%s.bak", dest_filename, GPGCONF_NAME); gnupg_rename_file (orig_filename, backup_filename, NULL); xfree (backup_filename); } leave: if (block) gnupg_unblock_all_signals (); xfree (line); xfree (src_filename); xfree (dest_filename); xfree (orig_filename); } /* Check whether USER matches the current user or one of its group. This function may change USER. Returns true is there is a match. */ static int key_matches_user_or_group (char *user) { char *group; if (*user == '*' && user[1] == 0) return 1; /* A single asterisk matches all users. */ group = strchr (user, ':'); if (group) *group++ = 0; #ifdef HAVE_W32_SYSTEM /* Under Windows we don't support groups. */ if (group && *group) gc_error (0, 0, _("Note that group specifications are ignored\n")); if (*user) { static char *my_name; if (!my_name) { char tmp[1]; DWORD size = 1; GetUserNameA (tmp, &size); my_name = xmalloc (size); if (!GetUserNameA (my_name, &size)) gc_error (1,0, "error getting current user name: %s", w32_strerror (-1)); } if (!strcmp (user, my_name)) return 1; /* Found. */ } #else /*!HAVE_W32_SYSTEM*/ /* First check whether the user matches. */ if (*user) { static char *my_name; if (!my_name) { struct passwd *pw = getpwuid ( getuid () ); if (!pw) gc_error (1, errno, "getpwuid failed for current user"); my_name = xstrdup (pw->pw_name); } if (!strcmp (user, my_name)) return 1; /* Found. */ } /* If that failed, check whether a group matches. */ if (group && *group) { static char *my_group; static char **my_supgroups; int n; if (!my_group) { struct group *gr = getgrgid ( getgid () ); if (!gr) gc_error (1, errno, "getgrgid failed for current user"); my_group = xstrdup (gr->gr_name); } if (!strcmp (group, my_group)) return 1; /* Found. */ if (!my_supgroups) { int ngids; gid_t *gids; ngids = getgroups (0, NULL); gids = xcalloc (ngids+1, sizeof *gids); ngids = getgroups (ngids, gids); if (ngids < 0) gc_error (1, errno, "getgroups failed for current user"); my_supgroups = xcalloc (ngids+1, sizeof *my_supgroups); for (n=0; n < ngids; n++) { struct group *gr = getgrgid ( gids[n] ); if (!gr) gc_error (1, errno, "getgrgid failed for supplementary group"); my_supgroups[n] = xstrdup (gr->gr_name); } xfree (gids); } for (n=0; my_supgroups[n]; n++) if (!strcmp (group, my_supgroups[n])) return 1; /* Found. */ } #endif /*!HAVE_W32_SYSTEM*/ return 0; /* No match. */ } /* Read and process the global configuration file for gpgconf. This optional file is used to update our internal tables at runtime and may also be used to set new default values. If FNAME is NULL the default name will be used. With UPDATE set to true the internal tables are actually updated; if not set, only a syntax check is done. If DEFAULTS is true the global options are written to the configuration files. If LISTFP is set, no changes are done but the configuration file is printed to LISTFP in a colon separated format. Returns 0 on success or if the config file is not present; -1 is returned on error. */ int gc_process_gpgconf_conf (const char *fname_arg, int update, int defaults, estream_t listfp) { int result = 0; char *line = NULL; size_t line_len = 0; ssize_t length; gpgrt_stream_t config; int lineno = 0; int in_rule = 0; int got_match = 0; int runtime[GC_COMPONENT_NR] = { 0 }; int component_id; char *fname; if (fname_arg) fname = xstrdup (fname_arg); else fname = make_filename (gnupg_sysconfdir (), GPGCONF_NAME EXTSEP_S "conf", NULL); config = gpgrt_fopen (fname, "r"); if (!config) { /* Do not print an error if the file is not available, except when running in syntax check mode. */ if (errno != ENOENT || !update) { gc_error (0, errno, "can't open global config file '%s'", fname); result = -1; } xfree (fname); return result; } while ((length = gpgrt_read_line (config, &line, &line_len, NULL)) > 0) { char *key, *compname, *option, *flags, *value; char *empty; gc_option_t *option_info = NULL; char *p; int is_continuation; lineno++; key = line; while (*key == ' ' || *key == '\t') key++; if (!*key || *key == '#' || *key == '\r' || *key == '\n') continue; is_continuation = (key != line); /* Parse the key field. */ if (!is_continuation && got_match) break; /* Finish after the first match. */ else if (!is_continuation) { in_rule = 0; for (p=key+1; *p && !strchr (" \t\r\n", *p); p++) ; if (!*p) { gc_error (0, 0, "missing rule at '%s', line %d", fname, lineno); result = -1; gpgconf_write_status (STATUS_WARNING, "gpgconf.conf %d file '%s' line %d " "missing rule", GPG_ERR_SYNTAX, fname, lineno); continue; } *p++ = 0; compname = p; } else if (!in_rule) { gc_error (0, 0, "continuation but no rule at '%s', line %d", fname, lineno); result = -1; continue; } else { compname = key; key = NULL; } in_rule = 1; /* Parse the component. */ while (*compname == ' ' || *compname == '\t') compname++; for (p=compname; *p && !strchr (" \t\r\n", *p); p++) ; if (p == compname) { gc_error (0, 0, "missing component at '%s', line %d", fname, lineno); gpgconf_write_status (STATUS_WARNING, "gpgconf.conf %d file '%s' line %d " " missing component", GPG_ERR_NO_NAME, fname, lineno); result = -1; continue; } empty = p; *p++ = 0; option = p; component_id = gc_component_find (compname); if (component_id < 0) { gc_error (0, 0, "unknown component at '%s', line %d", fname, lineno); gpgconf_write_status (STATUS_WARNING, "gpgconf.conf %d file '%s' line %d " "unknown component", GPG_ERR_UNKNOWN_NAME, fname, lineno); result = -1; } /* Parse the option name. */ while (*option == ' ' || *option == '\t') option++; for (p=option; *p && !strchr (" \t\r\n", *p); p++) ; if (p == option) { gc_error (0, 0, "missing option at '%s', line %d", fname, lineno); gpgconf_write_status (STATUS_WARNING, "gpgconf.conf %d file '%s' line %d " "missing option", GPG_ERR_INV_NAME, fname, lineno); result = -1; continue; } *p++ = 0; flags = p; if ( component_id != -1) { /* We need to make sure that we got the option list for the * component. */ if (!gc_component[component_id].options) gc_component_retrieve_options (component_id); option_info = find_option (component_id, option); if (!option_info) { gc_error (0, 0, "unknown option '%s' at '%s', line %d", option, fname, lineno); gpgconf_write_status (STATUS_WARNING, "gpgconf.conf %d file '%s' line %d " "unknown option", GPG_ERR_UNKNOWN_OPTION, fname, lineno); result = -1; } } /* Parse the optional flags. */ while (*flags == ' ' || *flags == '\t') flags++; if (*flags == '[') { flags++; p = strchr (flags, ']'); if (!p) { gc_error (0, 0, "syntax error in rule at '%s', line %d", fname, lineno); gpgconf_write_status (STATUS_WARNING, "gpgconf.conf %d file '%s' line %d " "syntax error in rule", GPG_ERR_SYNTAX, fname, lineno); result = -1; continue; } *p++ = 0; value = p; } else /* No flags given. */ { value = flags; flags = NULL; } /* Parse the optional value. */ while (*value == ' ' || *value == '\t') value++; for (p=value; *p && !strchr ("\r\n", *p); p++) ; if (p == value) value = empty; /* No value given; let it point to an empty string. */ else { /* Strip trailing white space. */ *p = 0; for (p--; p > value && (*p == ' ' || *p == '\t'); p--) *p = 0; } /* Check flag combinations. */ if (!flags) ; else if (!strcmp (flags, "default")) { if (*value) { gc_error (0, 0, "flag \"default\" may not be combined " "with a value at '%s', line %d", fname, lineno); result = -1; } } else if (!strcmp (flags, "change")) ; else if (!strcmp (flags, "no-change")) ; else { gc_error (0, 0, "unknown flag at '%s', line %d", fname, lineno); result = -1; } /* In list mode we print out all records. */ if (listfp && !result) { /* If this is a new ruleset, print a key record. */ if (!is_continuation) { char *group = strchr (key, ':'); if (group) { *group++ = 0; if ((p = strchr (group, ':'))) *p = 0; /* We better strip any extra stuff. */ } es_fprintf (listfp, "k:%s:", gc_percent_escape (key)); es_fprintf (listfp, "%s\n", group? gc_percent_escape (group):""); } /* All other lines are rule records. */ es_fprintf (listfp, "r:::%s:%s:%s:", gc_component[component_id].name, option_info->name? option_info->name : "", flags? flags : ""); if (value != empty) es_fprintf (listfp, "\"%s", gc_percent_escape (value)); es_putc ('\n', listfp); } /* Check whether the key matches but do this only if we are not running in syntax check mode. */ if ( update && !result && !listfp && (got_match || (key && key_matches_user_or_group (key))) ) { int newflags = 0; got_match = 1; /* Apply the flags from gpgconf.conf. */ if (!flags) ; else if (!strcmp (flags, "default")) newflags |= GC_OPT_FLAG_DEFAULT; else if (!strcmp (flags, "no-change")) option_info->no_change = 1; else if (!strcmp (flags, "change")) option_info->no_change = 0; if (defaults) { /* Here we explicitly allow updating the value again. */ if (newflags) { option_info->new_flags = 0; } if (*value) { xfree (option_info->new_value); option_info->new_value = NULL; } change_one_value (component_id, option_info, runtime, newflags, value, 0); } } } if (length < 0 || gpgrt_ferror (config)) { gc_error (0, errno, "error reading from '%s'", fname); result = -1; } if (gpgrt_fclose (config)) gc_error (0, errno, "error closing '%s'", fname); xfree (line); /* If it all worked, process the options. */ if (!result && update && defaults && !listfp) { /* We need to switch off the runtime update, so that we can do it later all at once. */ int save_opt_runtime = opt.runtime; opt.runtime = 0; for (component_id = 0; component_id < GC_COMPONENT_NR; component_id++) { gc_component_change_options (component_id, NULL, NULL, 0); } opt.runtime = save_opt_runtime; if (opt.runtime) { for (component_id = 0; component_id < GC_COMPONENT_NR; component_id++) if (runtime[component_id] && gc_component[component_id].runtime_change) (*gc_component[component_id].runtime_change) (0); } } xfree (fname); return result; } /* * Apply the profile FNAME to all known configure files. */ gpg_error_t gc_apply_profile (const char *fname) { gpg_error_t err; char *fname_buffer = NULL; char *line = NULL; size_t line_len = 0; ssize_t length; estream_t fp; int lineno = 0; int runtime[GC_COMPONENT_NR] = { 0 }; int component_id = -1; int skip_section = 0; int error_count = 0; int newflags; if (!fname) fname = "-"; if (!(!strcmp (fname, "-") || strchr (fname, '/') #ifdef HAVE_W32_SYSTEM || strchr (fname, '\\') #endif || strchr (fname, '.'))) { /* FNAME looks like a standard profile name. Check whether one * is installed and use that instead of the given file name. */ fname_buffer = xstrconcat (gnupg_datadir (), DIRSEP_S, fname, ".prf", NULL); if (!gnupg_access (fname_buffer, F_OK)) fname = fname_buffer; } fp = !strcmp (fname, "-")? es_stdin : es_fopen (fname, "r"); if (!fp) { err = gpg_error_from_syserror (); log_error ("can't open '%s': %s\n", fname, gpg_strerror (err)); return err; } if (opt.verbose) log_info ("applying profile '%s'\n", fname); err = 0; while ((length = es_read_line (fp, &line, &line_len, NULL)) > 0) { char *name, *flags, *value; gc_option_t *option_info = NULL; char *p; lineno++; name = line; while (*name == ' ' || *name == '\t') name++; if (!*name || *name == '#' || *name == '\r' || *name == '\n') continue; trim_trailing_spaces (name); /* Check whether this is a new section. */ if (*name == '[') { name++; skip_section = 0; /* New section: Get the name of the component. */ p = strchr (name, ']'); if (!p) { error_count++; log_info ("%s:%d:%d: error: syntax error in section tag\n", fname, lineno, (int)(name - line)); skip_section = 1; continue; } *p++ = 0; if (*p) log_info ("%s:%d:%d: warning: garbage after section tag\n", fname, lineno, (int)(p - line)); trim_spaces (name); component_id = gc_component_find (name); if (component_id < 0) { log_info ("%s:%d:%d: warning: skipping unknown section '%s'\n", fname, lineno, (int)(name - line), name ); skip_section = 1; } continue; } if (skip_section) continue; if (component_id < 0) { error_count++; log_info ("%s:%d:%d: error: not in a valid section\n", fname, lineno, (int)(name - line)); skip_section = 1; continue; } /* Parse the option name. */ for (p = name; *p && !spacep (p); p++) ; *p++ = 0; value = p; option_info = find_option (component_id, name); if (!option_info) { error_count++; log_info ("%s:%d:%d: error: unknown option '%s' in section '%s'\n", fname, lineno, (int)(name - line), name, gc_component[component_id].name); continue; } /* Parse the optional flags. */ trim_spaces (value); flags = value; if (*flags == '[') { flags++; p = strchr (flags, ']'); if (!p) { log_info ("%s:%d:%d: warning: invalid flag specification\n", fname, lineno, (int)(p - line)); continue; } *p++ = 0; value = p; trim_spaces (value); } else /* No flags given. */ flags = NULL; /* Set required defaults. */ if (gc_arg_type[option_info->arg_type].fallback == GC_ARG_TYPE_NONE && !*value) value = "1"; /* Check and save this option. */ newflags = 0; if (flags && !strcmp (flags, "default")) newflags |= GC_OPT_FLAG_DEFAULT; if (newflags) option_info->new_flags = 0; if (*value) { xfree (option_info->new_value); option_info->new_value = NULL; } change_one_value (component_id, option_info, runtime, newflags, value, 1); } if (length < 0 || es_ferror (fp)) { err = gpg_error_from_syserror (); error_count++; log_error (_("%s:%u: read error: %s\n"), fname, lineno, gpg_strerror (err)); } if (es_fclose (fp)) log_error (_("error closing '%s'\n"), fname); if (error_count) log_error (_("error parsing '%s'\n"), fname); xfree (line); /* If it all worked, process the options. */ if (!err) { /* We need to switch off the runtime update, so that we can do it later all at once. */ int save_opt_runtime = opt.runtime; opt.runtime = 0; for (component_id = 0; component_id < GC_COMPONENT_NR; component_id++) { gc_component_change_options (component_id, NULL, NULL, 1); } opt.runtime = save_opt_runtime; if (opt.runtime) { for (component_id = 0; component_id < GC_COMPONENT_NR; component_id++) if (runtime[component_id] && gc_component[component_id].runtime_change) (*gc_component[component_id].runtime_change) (0); } } xfree (fname_buffer); return err; } diff --git a/tools/gpgconf.c b/tools/gpgconf.c index 6dcdc9f3c..522ce517b 100644 --- a/tools/gpgconf.c +++ b/tools/gpgconf.c @@ -1,1672 +1,1676 @@ /* gpgconf.c - Configuration utility for GnuPG * Copyright (C) 2003, 2007, 2009, 2011 Free Software Foundation, Inc. * Copyright (C) 2016 g10 Code GmbH. * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include #include #include #include #define INCLUDED_BY_MAIN_MODULE 1 #include "gpgconf.h" #include "../common/i18n.h" #include "../common/sysutils.h" #include "../common/init.h" #include "../common/status.h" #include "../common/exechelp.h" #ifdef HAVE_W32_SYSTEM #include #endif /* Constants to identify the commands and options. */ enum cmd_and_opt_values { aNull = 0, oDryRun = 'n', oOutput = 'o', oQuiet = 'q', oVerbose = 'v', oRuntime = 'r', oComponent = 'c', oNull = '0', aListDirs = 'L', aKill = 'K', aReload = 'R', aShowVersions = 'V', aShowConfigs = 'X', oNoVerbose = 500, oHomedir, oBuilddir, oStatusFD, oShowSocket, oChUid, aListComponents, aCheckPrograms, aListOptions, aChangeOptions, aCheckOptions, aApplyDefaults, aListConfig, aCheckConfig, aQuerySWDB, aLaunch, aCreateSocketDir, aRemoveSocketDir, aApplyProfile, aShowCodepages }; /* The list of commands and options. */ static gpgrt_opt_t opts[] = { { 300, NULL, 0, N_("@Commands:\n ") }, { aListComponents, "list-components", 256, N_("list all components") }, { aCheckPrograms, "check-programs", 256, N_("check all programs") }, { aListOptions, "list-options", 256, N_("|COMPONENT|list options") }, { aChangeOptions, "change-options", 256, N_("|COMPONENT|change options") }, { aCheckOptions, "check-options", 256, N_("|COMPONENT|check options") }, { aApplyDefaults, "apply-defaults", 256, N_("apply global default values") }, { aApplyProfile, "apply-profile", 256, N_("|FILE|update configuration files using FILE") }, { aListDirs, "list-dirs", 256, N_("get the configuration directories for @GPGCONF@") }, { aListConfig, "list-config", 256, N_("list global configuration file") }, { aCheckConfig, "check-config", 256, N_("check global configuration file") }, { aQuerySWDB, "query-swdb", 256, N_("query the software version database") }, { aReload, "reload", 256, N_("reload all or a given component")}, { aLaunch, "launch", 256, N_("launch a given component")}, { aKill, "kill", 256, N_("kill a given component")}, { aCreateSocketDir, "create-socketdir", 256, "@"}, { aRemoveSocketDir, "remove-socketdir", 256, "@"}, ARGPARSE_c (aShowVersions, "show-versions", ""), ARGPARSE_c (aShowConfigs, "show-configs", ""), ARGPARSE_c (aShowCodepages, "show-codepages", "@"), { 301, NULL, 0, N_("@\nOptions:\n ") }, { oOutput, "output", 2, N_("use as output file") }, { oVerbose, "verbose", 0, N_("verbose") }, { oQuiet, "quiet", 0, N_("quiet") }, { oDryRun, "dry-run", 0, N_("do not make any changes") }, { oRuntime, "runtime", 0, N_("activate changes at runtime, if possible") }, ARGPARSE_s_i (oStatusFD, "status-fd", N_("|FD|write status info to this FD")), /* hidden options */ { oHomedir, "homedir", 2, "@" }, { oBuilddir, "build-prefix", 2, "@" }, { oNull, "null", 0, "@" }, { oNoVerbose, "no-verbose", 0, "@"}, ARGPARSE_s_n (oShowSocket, "show-socket", "@"), ARGPARSE_s_s (oChUid, "chuid", "@"), ARGPARSE_end(), }; #define CUTLINE_FMT \ "--8<---------------cut here---------------%s------------->8---\n" /* The stream to output the status information. Status Output is disabled if * this is NULL. */ static estream_t statusfp; static void show_versions (estream_t fp); static void show_configs (estream_t fp); /* Print usage information and provide strings for help. */ static const char * my_strusage( int level ) { const char *p; switch (level) { case 9: p = "GPL-3.0-or-later"; break; case 11: p = "@GPGCONF@ (@GNUPG@)"; break; case 13: p = VERSION; break; case 14: p = GNUPG_DEF_COPYRIGHT_LINE; break; case 17: p = PRINTABLE_OS_NAME; break; case 19: p = _("Please report bugs to <@EMAIL@>.\n"); break; case 1: case 40: p = _("Usage: @GPGCONF@ [options] (-h for help)"); break; case 41: p = _("Syntax: @GPGCONF@ [options]\n" "Manage configuration options for tools of the @GNUPG@ system\n"); break; default: p = NULL; break; } return p; } /* Return the fp for the output. This is usually stdout unless --output has been used. In the latter case this function opens that file. */ static estream_t get_outfp (estream_t *fp) { if (!*fp) { if (opt.outfile) { *fp = es_fopen (opt.outfile, "w"); if (!*fp) gc_error (1, errno, "can not open '%s'", opt.outfile); } else *fp = es_stdout; } return *fp; } /* Set the status FD. */ static void set_status_fd (int fd) { static int last_fd = -1; if (fd != -1 && last_fd == fd) return; if (statusfp && statusfp != es_stdout && statusfp != es_stderr) es_fclose (statusfp); statusfp = NULL; if (fd == -1) return; if (fd == 1) statusfp = es_stdout; else if (fd == 2) statusfp = es_stderr; else statusfp = es_fdopen (fd, "w"); if (!statusfp) { log_fatal ("can't open fd %d for status output: %s\n", fd, gpg_strerror (gpg_error_from_syserror ())); } last_fd = fd; } /* Write a status line with code NO followed by the output of the * printf style FORMAT. The caller needs to make sure that LFs and * CRs are not printed. */ void gpgconf_write_status (int no, const char *format, ...) { va_list arg_ptr; if (!statusfp) return; /* Not enabled. */ es_fputs ("[GNUPG:] ", statusfp); es_fputs (get_status_string (no), statusfp); if (format) { es_putc (' ', statusfp); va_start (arg_ptr, format); es_vfprintf (statusfp, format, arg_ptr); va_end (arg_ptr); } es_putc ('\n', statusfp); } static void list_dirs (estream_t fp, char **names, int special) { static struct { const char *name; const char *(*fnc)(void); const char *extra; } list[] = { { "sysconfdir", gnupg_sysconfdir, NULL }, { "bindir", gnupg_bindir, NULL }, { "libexecdir", gnupg_libexecdir, NULL }, { "libdir", gnupg_libdir, NULL }, { "datadir", gnupg_datadir, NULL }, { "localedir", gnupg_localedir, NULL }, { "socketdir", gnupg_socketdir, NULL }, { "dirmngr-socket", dirmngr_socket_name, NULL,}, { "keyboxd-socket", keyboxd_socket_name, NULL,}, { "agent-ssh-socket", gnupg_socketdir, GPG_AGENT_SSH_SOCK_NAME }, { "agent-extra-socket", gnupg_socketdir, GPG_AGENT_EXTRA_SOCK_NAME }, { "agent-browser-socket",gnupg_socketdir, GPG_AGENT_BROWSER_SOCK_NAME }, { "agent-socket", gnupg_socketdir, GPG_AGENT_SOCK_NAME }, { "homedir", gnupg_homedir, NULL } }; int idx, j; char *tmp; const char *s; for (idx = 0; idx < DIM (list); idx++) { s = list[idx].fnc (); if (list[idx].extra) { tmp = make_filename (s, list[idx].extra, NULL); s = tmp; } else tmp = NULL; if (!names) es_fprintf (fp, "%s:%s\n", list[idx].name, gc_percent_escape (s)); else { for (j=0; names[j]; j++) if (!strcmp (names[j], list[idx].name)) { es_fputs (s, fp); es_putc (opt.null? '\0':'\n', fp); } } xfree (tmp); } #ifdef HAVE_W32_SYSTEM tmp = read_w32_registry_string (NULL, GNUPG_REGISTRY_DIR, "HomeDir"); if (tmp) { int hkcu = 0; int hklm = 0; xfree (tmp); if ((tmp = read_w32_registry_string ("HKEY_CURRENT_USER", GNUPG_REGISTRY_DIR, "HomeDir"))) { xfree (tmp); hkcu = 1; } if ((tmp = read_w32_registry_string ("HKEY_LOCAL_MACHINE", GNUPG_REGISTRY_DIR, "HomeDir"))) { xfree (tmp); hklm = 1; } es_fflush (fp); if (special) es_fprintf (fp, "\n" "### Note: homedir taken from registry key %s%s\\%s:%s\n" "\n", hkcu?"HKCU":"", hklm?"HKLM":"", GNUPG_REGISTRY_DIR, "HomeDir"); else log_info ("Warning: homedir taken from registry key (%s:%s) in%s%s\n", GNUPG_REGISTRY_DIR, "HomeDir", hkcu?" HKCU":"", hklm?" HKLM":""); } else if ((tmp = read_w32_registry_string (NULL, GNUPG_REGISTRY_DIR, NULL))) { xfree (tmp); es_fflush (fp); if (special) es_fprintf (fp, "\n" "### Note: registry %s without value in HKCU or HKLM\n" "\n", GNUPG_REGISTRY_DIR); else log_info ("Warning: registry key (%s) without value in HKCU or HKLM\n", GNUPG_REGISTRY_DIR); } #else /*!HAVE_W32_SYSTEM*/ (void)special; #endif /*!HAVE_W32_SYSTEM*/ } /* Check whether NAME is valid argument for query_swdb(). Valid names * start with a letter and contain only alphanumeric characters or an * underscore. */ static int valid_swdb_name_p (const char *name) { if (!name || !*name || !alphap (name)) return 0; for (name++; *name; name++) if (!alnump (name) && *name != '_') return 0; return 1; } /* Query the SWDB file. If necessary and possible this functions asks * the dirmngr to load an updated version of that file. The caller * needs to provide the NAME to query (e.g. "gnupg", "libgcrypt") and * optional the currently installed version in CURRENT_VERSION. The * output written to OUT is a colon delimited line with these fields: * * name :: The name of the package * curvers:: The installed version if given. * status :: This value tells the status of the software package * '-' :: No information available * (error or CURRENT_VERSION not given) * '?' :: Unknown NAME * 'u' :: Update available * 'c' :: The version is Current * 'n' :: The current version is already Newer than the * available one. * urgency :: If the value is greater than zero an urgent update is required. * error :: 0 on success or an gpg_err_code_t * Common codes seen: * GPG_ERR_TOO_OLD :: The SWDB file is to old to be used. * GPG_ERR_ENOENT :: The SWDB file is not available. * GPG_ERR_BAD_SIGNATURE :: Corrupted SWDB file. * filedate:: Date of the swdb file (yyyymmddThhmmss) * verified:: Date we checked the validity of the file (yyyyymmddThhmmss) * version :: The version string from the swdb. * reldate :: Release date of that version (yyyymmddThhmmss) * size :: Size of the package in bytes. * hash :: SHA-2 hash of the package. * */ static void query_swdb (estream_t out, const char *name, const char *current_version) { gpg_error_t err; const char *search_name; char *fname = NULL; estream_t fp = NULL; char *line = NULL; char *self_version = NULL; size_t length_of_line = 0; size_t maxlen; ssize_t len; const char *fields[2]; char *p; gnupg_isotime_t filedate = {0}; gnupg_isotime_t verified = {0}; char *value_ver = NULL; gnupg_isotime_t value_date = {0}; char *value_size = NULL; char *value_sha2 = NULL; unsigned long value_size_ul = 0; int status, i; if (!valid_swdb_name_p (name)) { log_error ("error in package name '%s': %s\n", name, gpg_strerror (GPG_ERR_INV_NAME)); goto leave; } if (!strcmp (name, "gnupg")) search_name = GNUPG_SWDB_TAG; else if (!strcmp (name, "gnupg1")) search_name = "gnupg1"; else search_name = name; if (!current_version && !strcmp (name, "gnupg")) { /* Use our own version but string a possible beta string. */ self_version = xstrdup (PACKAGE_VERSION); p = strchr (self_version, '-'); if (p) *p = 0; current_version = self_version; } if (current_version && (strchr (current_version, ':') || compare_version_strings (current_version, NULL))) { log_error ("error in version string '%s': %s\n", current_version, gpg_strerror (GPG_ERR_INV_ARG)); goto leave; } fname = make_filename (gnupg_homedir (), "swdb.lst", NULL); fp = es_fopen (fname, "r"); if (!fp) { err = gpg_error_from_syserror (); es_fprintf (out, "%s:%s:-::%u:::::::\n", name, current_version? current_version : "", gpg_err_code (err)); if (gpg_err_code (err) != GPG_ERR_ENOENT) log_error (_("error opening '%s': %s\n"), fname, gpg_strerror (err)); goto leave; } /* Note that the parser uses the first occurrence of a matching * values and ignores possible duplicated values. */ maxlen = 2048; /* Set limit. */ while ((len = es_read_line (fp, &line, &length_of_line, &maxlen)) > 0) { if (!maxlen) { err = gpg_error (GPG_ERR_LINE_TOO_LONG); log_error (_("error reading '%s': %s\n"), fname, gpg_strerror (err)); goto leave; } /* Strip newline and carriage return, if present. */ while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) line[--len] = '\0'; if (split_fields (line, fields, DIM (fields)) < DIM(fields)) continue; /* Skip empty lines and names w/o a value. */ if (*fields[0] == '#') continue; /* Skip comments. */ /* Record the meta data. */ if (!*filedate && !strcmp (fields[0], ".filedate")) { string2isotime (filedate, fields[1]); continue; } if (!*verified && !strcmp (fields[0], ".verified")) { string2isotime (verified, fields[1]); continue; } /* Tokenize the name. */ p = strrchr (fields[0], '_'); if (!p) continue; /* Name w/o an underscore. */ *p++ = 0; /* Wait for the requested name. */ if (!strcmp (fields[0], search_name)) { if (!strcmp (p, "ver") && !value_ver) value_ver = xstrdup (fields[1]); else if (!strcmp (p, "date") && !*value_date) string2isotime (value_date, fields[1]); else if (!strcmp (p, "size") && !value_size) value_size = xstrdup (fields[1]); else if (!strcmp (p, "sha2") && !value_sha2) value_sha2 = xstrdup (fields[1]); } } if (len < 0 || es_ferror (fp)) { err = gpg_error_from_syserror (); log_error (_("error reading '%s': %s\n"), fname, gpg_strerror (err)); goto leave; } if (!*filedate || !*verified) { err = gpg_error (GPG_ERR_INV_TIME); es_fprintf (out, "%s:%s:-::%u:::::::\n", name, current_version? current_version : "", gpg_err_code (err)); goto leave; } if (!value_ver) { es_fprintf (out, "%s:%s:?:::::::::\n", name, current_version? current_version : ""); goto leave; } if (value_size) { gpg_err_set_errno (0); value_size_ul = strtoul (value_size, &p, 10); if (errno) value_size_ul = 0; else if (*p == 'k') value_size_ul *= 1024; } err = 0; status = '-'; if (compare_version_strings (value_ver, NULL)) err = gpg_error (GPG_ERR_INV_VALUE); else if (!current_version) ; else if (!(i = compare_version_strings (value_ver, current_version))) status = 'c'; else if (i > 0) status = 'u'; else status = 'n'; es_fprintf (out, "%s:%s:%c::%d:%s:%s:%s:%s:%lu:%s:\n", name, current_version? current_version : "", status, err, filedate, verified, value_ver, value_date, value_size_ul, value_sha2? value_sha2 : ""); leave: xfree (value_ver); xfree (value_size); xfree (value_sha2); xfree (line); es_fclose (fp); xfree (fname); xfree (self_version); } /* gpgconf main. */ int main (int argc, char **argv) { gpg_error_t err; gpgrt_argparse_t pargs; const char *fname; int no_more_options = 0; enum cmd_and_opt_values cmd = 0; estream_t outfp = NULL; int show_socket = 0; const char *changeuser = NULL; early_system_init (); gnupg_reopen_std (GPGCONF_NAME); gpgrt_set_strusage (my_strusage); log_set_prefix (GPGCONF_NAME, GPGRT_LOG_WITH_PREFIX|GPGRT_LOG_NO_REGISTRY); /* Make sure that our subsystems are ready. */ i18n_init(); init_common_subsystems (&argc, &argv); gc_components_init (); /* Parse the command line. */ pargs.argc = &argc; pargs.argv = &argv; pargs.flags = ARGPARSE_FLAG_KEEP; while (!no_more_options && gpgrt_argparse (NULL, &pargs, opts)) { switch (pargs.r_opt) { case oOutput: opt.outfile = pargs.r.ret_str; break; case oQuiet: opt.quiet = 1; break; case oDryRun: opt.dry_run = 1; break; case oRuntime: opt.runtime = 1; break; case oVerbose: opt.verbose++; break; case oNoVerbose: opt.verbose = 0; break; case oHomedir: gnupg_set_homedir (pargs.r.ret_str); break; case oBuilddir: gnupg_set_builddir (pargs.r.ret_str); break; case oNull: opt.null = 1; break; case oStatusFD: set_status_fd (translate_sys2libc_fd_int (pargs.r.ret_int, 1)); break; case oShowSocket: show_socket = 1; break; case oChUid: changeuser = pargs.r.ret_str; break; case aListDirs: case aListComponents: case aCheckPrograms: case aListOptions: case aChangeOptions: case aCheckOptions: case aApplyDefaults: case aApplyProfile: case aListConfig: case aCheckConfig: case aQuerySWDB: case aReload: case aLaunch: case aKill: case aCreateSocketDir: case aRemoveSocketDir: case aShowVersions: case aShowConfigs: case aShowCodepages: cmd = pargs.r_opt; break; default: pargs.err = 2; break; } } gpgrt_argparse (NULL, &pargs, NULL); /* Release internal state. */ if (log_get_errorcount (0)) gpgconf_failure (GPG_ERR_USER_2); /* Print a warning if an argument looks like an option. */ if (!opt.quiet && !(pargs.flags & ARGPARSE_FLAG_STOP_SEEN)) { int i; for (i=0; i < argc; i++) if (argv[i][0] == '-' && argv[i][1] == '-') log_info (_("Note: '%s' is not considered an option\n"), argv[i]); } fname = argc ? *argv : NULL; /* If requested switch to the requested user or die. */ if (changeuser && (err = gnupg_chuid (changeuser, 0))) gpgconf_failure (err); /* Set the configuraton directories for use by gpgrt_argparser. We * don't have a configuration file for this program but we have code * which reads the component's config files. */ gpgrt_set_confdir (GPGRT_CONFDIR_SYS, gnupg_sysconfdir ()); gpgrt_set_confdir (GPGRT_CONFDIR_USER, gnupg_homedir ()); switch (cmd) { case aListComponents: default: /* List all components. */ gc_component_list_components (get_outfp (&outfp)); break; case aCheckPrograms: /* Check all programs. */ gc_check_programs (get_outfp (&outfp)); break; case aListOptions: case aChangeOptions: case aCheckOptions: if (!fname) { es_fprintf (es_stderr, _("usage: %s [options] "), GPGCONF_NAME); es_putc ('\n', es_stderr); es_fputs (_("Need one component argument"), es_stderr); es_putc ('\n', es_stderr); gpgconf_failure (GPG_ERR_USER_2); } else { int idx = gc_component_find (fname); if (idx < 0) { es_fputs (_("Component not found"), es_stderr); es_putc ('\n', es_stderr); gpgconf_failure (0); } if (cmd == aCheckOptions) gc_component_check_options (idx, get_outfp (&outfp), NULL); else { gc_component_retrieve_options (idx); if (gc_process_gpgconf_conf (NULL, 1, 0, NULL)) gpgconf_failure (0); if (cmd == aListOptions) gc_component_list_options (idx, get_outfp (&outfp)); else if (cmd == aChangeOptions) gc_component_change_options (idx, es_stdin, get_outfp (&outfp), 0); } } break; case aLaunch: case aKill: if (!fname) { es_fprintf (es_stderr, _("usage: %s [options] "), GPGCONF_NAME); es_putc ('\n', es_stderr); es_fputs (_("Need one component argument"), es_stderr); es_putc ('\n', es_stderr); gpgconf_failure (GPG_ERR_USER_2); } else if (!strcmp (fname, "all")) { if (cmd == aLaunch) { if (gc_component_launch (-1)) gpgconf_failure (0); } else { gc_component_kill (-1); } } else { /* Launch/Kill a given component. */ int idx; idx = gc_component_find (fname); if (idx < 0) { es_fputs (_("Component not found"), es_stderr); es_putc ('\n', es_stderr); gpgconf_failure (0); } else if (cmd == aLaunch) { err = gc_component_launch (idx); if (show_socket) { char *names[2]; if (idx == GC_COMPONENT_GPG_AGENT) names[0] = "agent-socket"; else if (idx == GC_COMPONENT_DIRMNGR) names[0] = "dirmngr-socket"; else if (idx == GC_COMPONENT_KEYBOXD) names[0] = "keyboxd-socket"; else names[0] = NULL; names[1] = NULL; get_outfp (&outfp); list_dirs (outfp, names, 0); } if (err) gpgconf_failure (0); } else { /* We don't error out if the kill failed because this command should do nothing if the component is not running. */ gc_component_kill (idx); } } break; case aReload: if (!fname || !strcmp (fname, "all")) { /* Reload all. */ gc_component_reload (-1); } else { /* Reload given component. */ int idx; idx = gc_component_find (fname); if (idx < 0) { es_fputs (_("Component not found"), es_stderr); es_putc ('\n', es_stderr); gpgconf_failure (0); } else { gc_component_reload (idx); } } break; case aListConfig: if (gc_process_gpgconf_conf (fname, 0, 0, get_outfp (&outfp))) gpgconf_failure (0); break; case aCheckConfig: if (gc_process_gpgconf_conf (fname, 0, 0, NULL)) gpgconf_failure (0); break; case aApplyDefaults: if (fname) { es_fprintf (es_stderr, _("usage: %s [options] "), GPGCONF_NAME); es_putc ('\n', es_stderr); es_fputs (_("No argument allowed"), es_stderr); es_putc ('\n', es_stderr); gpgconf_failure (GPG_ERR_USER_2); } if (!opt.dry_run && gnupg_access (gnupg_homedir (), F_OK)) gnupg_maybe_make_homedir (gnupg_homedir (), opt.quiet); gc_component_retrieve_options (-1); if (gc_process_gpgconf_conf (NULL, 1, 1, NULL)) gpgconf_failure (0); break; case aApplyProfile: if (!opt.dry_run && gnupg_access (gnupg_homedir (), F_OK)) gnupg_maybe_make_homedir (gnupg_homedir (), opt.quiet); gc_component_retrieve_options (-1); if (gc_apply_profile (fname)) gpgconf_failure (0); break; case aListDirs: /* Show the system configuration directories for gpgconf. */ get_outfp (&outfp); list_dirs (outfp, argc? argv : NULL, 0); break; case aQuerySWDB: /* Query the software version database. */ if (!fname || argc > 2) { es_fprintf (es_stderr, "usage: %s --query-swdb NAME [VERSION]\n", GPGCONF_NAME); gpgconf_failure (GPG_ERR_USER_2); } get_outfp (&outfp); query_swdb (outfp, fname, argc > 1? argv[1] : NULL); break; case aCreateSocketDir: { char *socketdir; unsigned int flags; /* Make sure that the top /run/user/UID/gnupg dir has been * created. */ gnupg_socketdir (); /* Check the /var/run dir. */ socketdir = _gnupg_socketdir_internal (1, &flags); if ((flags & 64) && !opt.dry_run) { /* No sub dir - create it. */ if (gnupg_mkdir (socketdir, "-rwx")) gc_error (1, errno, "error creating '%s'", socketdir); /* Try again. */ xfree (socketdir); socketdir = _gnupg_socketdir_internal (1, &flags); } /* Give some info. */ if ( (flags & ~32) || opt.verbose || opt.dry_run) { log_info ("socketdir is '%s'\n", socketdir); if ((flags & 1)) log_info ("\tgeneral error\n"); if ((flags & 2)) log_info ("\tno /run/user dir\n"); if ((flags & 4)) log_info ("\tbad permissions\n"); if ((flags & 8)) log_info ("\tbad permissions (subdir)\n"); if ((flags & 16)) log_info ("\tmkdir failed\n"); if ((flags & 32)) log_info ("\tnon-default homedir\n"); if ((flags & 64)) log_info ("\tno such subdir\n"); if ((flags & 128)) log_info ("\tusing homedir as fallback\n"); } if ((flags & ~32) && !opt.dry_run) gc_error (1, 0, "error creating socket directory"); xfree (socketdir); } break; case aRemoveSocketDir: { char *socketdir; unsigned int flags; /* Check the /var/run dir. */ socketdir = _gnupg_socketdir_internal (1, &flags); if ((flags & 128)) log_info ("ignoring request to remove non /run/user socket dir\n"); else if (opt.dry_run) ; else if (gnupg_rmdir (socketdir)) { /* If the director is not empty we first try to delete * socket files. */ err = gpg_error_from_syserror (); if (gpg_err_code (err) == GPG_ERR_ENOTEMPTY || gpg_err_code (err) == GPG_ERR_EEXIST) { static const char * const names[] = { GPG_AGENT_SOCK_NAME, GPG_AGENT_EXTRA_SOCK_NAME, GPG_AGENT_BROWSER_SOCK_NAME, GPG_AGENT_SSH_SOCK_NAME, SCDAEMON_SOCK_NAME, KEYBOXD_SOCK_NAME, DIRMNGR_SOCK_NAME }; int i; char *p; for (i=0; i < DIM(names); i++) { p = strconcat (socketdir , "/", names[i], NULL); if (p) gnupg_remove (p); xfree (p); } if (gnupg_rmdir (socketdir)) gc_error (1, 0, "error removing '%s': %s", socketdir, gpg_strerror (err)); } else if (gpg_err_code (err) == GPG_ERR_ENOENT) gc_error (0, 0, "warning: removing '%s' failed: %s", socketdir, gpg_strerror (err)); else gc_error (1, 0, "error removing '%s': %s", socketdir, gpg_strerror (err)); } xfree (socketdir); } break; case aShowVersions: { get_outfp (&outfp); show_versions (outfp); } break; case aShowConfigs: { get_outfp (&outfp); show_configs (outfp); } break; case aShowCodepages: #ifdef HAVE_W32_SYSTEM { get_outfp (&outfp); if (GetConsoleCP () != GetConsoleOutputCP ()) es_fprintf (outfp, "Console: CP%u/CP%u\n", GetConsoleCP (), GetConsoleOutputCP ()); else es_fprintf (outfp, "Console: CP%u\n", GetConsoleCP ()); es_fprintf (outfp, "ANSI: CP%u\n", GetACP ()); es_fprintf (outfp, "OEM: CP%u\n", GetOEMCP ()); } #endif break; } if (outfp != es_stdout) if (es_fclose (outfp)) gc_error (1, errno, "error closing '%s'", opt.outfile); if (log_get_errorcount (0)) gpgconf_failure (0); else gpgconf_write_status (STATUS_SUCCESS, NULL); return 0; } void gpgconf_failure (gpg_error_t err) { log_flush (); if (!err) err = gpg_error (GPG_ERR_GENERAL); gpgconf_write_status (STATUS_FAILURE, "- %u", gpg_err_code (err) == GPG_ERR_USER_2? GPG_ERR_EINVAL : err); exit (gpg_err_code (err) == GPG_ERR_USER_2? 2 : 1); } /* Parse the revision part from the extended version blurb. */ static const char * get_revision_from_blurb (const char *blurb, int *r_len) { const char *s = blurb? blurb : ""; int n; for (; *s; s++) if (*s == '\n' && s[1] == '(') break; if (s) { s += 2; for (n=0; s[n] && s[n] != ' '; n++) ; } else { s = "?"; n = 1; } *r_len = n; return s; } static void show_version_gnupg (estream_t fp, const char *prefix) { char *fname, *p; size_t n; estream_t verfp; char line[100]; es_fprintf (fp, "%s%sGnuPG %s (%s)\n%s%s\n", prefix, *prefix?"":"* ", gpgrt_strusage (13), BUILD_REVISION, prefix, gpgrt_strusage (17)); /* Show the GnuPG VS-Desktop version in --show-configs mode */ if (prefix && *prefix == '#') { fname = make_filename (gnupg_bindir (), NULL); n = strlen (fname); if (n > 10 && (!ascii_strcasecmp (fname + n - 10, "/GnuPG/bin") || !ascii_strcasecmp (fname + n - 10, "\\GnuPG\\bin"))) { /* Append VERSION to the ../../ direcory. Note that VERSION * is only 7 bytes and thus fits. */ strcpy (fname + n - 9, "VERSION"); verfp = es_fopen (fname, "r"); if (!verfp) es_fprintf (fp, "%s[VERSION file not found]\n", prefix); else if (!es_fgets (line, sizeof line, verfp)) es_fprintf (fp, "%s[VERSION file is empty]\n", prefix); else { trim_spaces (line); for (p=line; *p; p++) if (*p < ' ' || *p > '~' || *p == '[') *p = '?'; es_fprintf (fp, "%s%s\n", prefix, line); } es_fclose (verfp); } xfree (fname); } #ifdef HAVE_W32_SYSTEM { OSVERSIONINFO osvi = { sizeof (osvi) }; GetVersionEx (&osvi); es_fprintf (fp, "%sWindows %lu.%lu build %lu%s%s%s\n", prefix, (unsigned long)osvi.dwMajorVersion, (unsigned long)osvi.dwMinorVersion, (unsigned long)osvi.dwBuildNumber, *osvi.szCSDVersion? " (":"", osvi.szCSDVersion, *osvi.szCSDVersion? ")":"" ); } #endif /*HAVE_W32_SYSTEM*/ } static void show_version_libgcrypt (estream_t fp) { const char *s; int n; s = get_revision_from_blurb (gcry_check_version ("\x01\x01"), &n); es_fprintf (fp, "* Libgcrypt %s (%.*s)\n", gcry_check_version (NULL), n, s); s = gcry_get_config (0, NULL); if (s) es_fputs (s, fp); } static void show_version_gpgrt (estream_t fp) { const char *s; int n; s = get_revision_from_blurb (gpg_error_check_version ("\x01\x01"), &n); es_fprintf (fp, "* GpgRT %s (%.*s)\n", gpg_error_check_version (NULL), n, s); } /* Printing version information for other libraries is problematic * because we don't want to link gpgconf to all these libraries. The * best solution is delegating this to dirmngr which uses libassuan, * libksba, libnpth and ntbtls anyway. */ static void show_versions_via_dirmngr (estream_t fp) { gpg_error_t err; const char *pgmname; const char *argv[2]; estream_t outfp; - pid_t pid; + gnupg_process_t proc; char *line = NULL; size_t line_len = 0; ssize_t length; - int exitcode; pgmname = gnupg_module_name (GNUPG_MODULE_NAME_DIRMNGR); argv[0] = "--gpgconf-versions"; argv[1] = NULL; - err = gnupg_spawn_process (pgmname, argv, NULL, 0, - NULL, &outfp, NULL, &pid); + err = gnupg_process_spawn (pgmname, argv, + GNUPG_PROCESS_STDOUT_PIPE, + NULL, NULL, &proc); if (err) { log_error ("error spawning %s: %s", pgmname, gpg_strerror (err)); es_fprintf (fp, "[error: can't get further info]\n"); return; } + gnupg_process_get_streams (proc, 0, NULL, &outfp, NULL); while ((length = es_read_line (outfp, &line, &line_len, NULL)) > 0) { /* Strip newline and carriage return, if present. */ while (length > 0 && (line[length - 1] == '\n' || line[length - 1] == '\r')) line[--length] = '\0'; es_fprintf (fp, "%s\n", line); } if (length < 0 || es_ferror (outfp)) { err = gpg_error_from_syserror (); log_error ("error reading from %s: %s\n", pgmname, gpg_strerror (err)); } if (es_fclose (outfp)) { err = gpg_error_from_syserror (); log_error ("error closing output stream of %s: %s\n", pgmname, gpg_strerror (err)); } - err = gnupg_wait_process (pgmname, pid, 1, &exitcode); - if (err) + err = gnupg_process_wait (proc, 1); + if (!err) { + int exitcode; + + gnupg_process_ctl (proc, GNUPG_PROCESS_GET_EXIT_ID, &exitcode); log_error ("running %s failed (exitcode=%d): %s\n", pgmname, exitcode, gpg_strerror (err)); es_fprintf (fp, "[error: can't get further info]\n"); } - gnupg_release_process (pid); + gnupg_process_release (proc); xfree (line); } /* Show all kind of version information. */ static void show_versions (estream_t fp) { show_version_gnupg (fp, ""); es_fputc ('\n', fp); show_version_libgcrypt (fp); es_fputc ('\n', fp); show_version_gpgrt (fp); es_fputc ('\n', fp); show_versions_via_dirmngr (fp); } /* Copy data from file SRC to DST. Returns 0 on success or an error * code on failure. If LISTP is not NULL, that strlist is updated * with the variabale or registry key names detected. Flag bit 0 * indicates a registry entry. */ static gpg_error_t my_copy_file (estream_t src, estream_t dst, strlist_t *listp) { gpg_error_t err; char *line = NULL; size_t line_len = 0; ssize_t length; int written; while ((length = es_read_line (src, &line, &line_len, NULL)) > 0) { /* Strip newline and carriage return, if present. */ written = gpgrt_fwrite (line, 1, length, dst); if (written != length) return gpg_error_from_syserror (); trim_spaces (line); if (*line == '[' && listp) { char **tokens; char *p; for (p=line+1; *p; p++) if (*p != ' ' && *p != '\t') break; if (*p && p[strlen (p)-1] == ']') p[strlen (p)-1] = 0; tokens = strtokenize (p, " \t"); if (!tokens) { err = gpg_error_from_syserror (); log_error ("strtokenize failed: %s\n", gpg_strerror (err)); return err; } /* Check whether we have a getreg or getenv statement and * store the third token to later retrieval. */ if (tokens[0] && tokens[1] && tokens[2] && (!strcmp (tokens[0], "getreg") || !strcmp (tokens[0], "getenv"))) { int isreg = (tokens[0][3] == 'r'); strlist_t sl = *listp; for (sl = *listp; sl; sl = sl->next) if (!strcmp (sl->d, tokens[2]) && (sl->flags & 1) == isreg) break; if (!sl) /* Not yet in the respective list. */ { sl = add_to_strlist (listp, tokens[2]); if (isreg) sl->flags = 1; } } xfree (tokens); } } if (length < 0 || es_ferror (src)) return gpg_error_from_syserror (); if (gpgrt_fflush (dst)) return gpg_error_from_syserror (); return 0; } /* Helper for show_configs */ static void show_configs_one_file (const char *fname, int global, estream_t outfp, strlist_t *listp) { gpg_error_t err; estream_t fp; fp = es_fopen (fname, "r"); if (!fp) { err = gpg_error_from_syserror (); es_fprintf (outfp, "###\n### %s config \"%s\": %s\n###\n", global? "global":"local", fname, (gpg_err_code (err) == GPG_ERR_ENOENT)? "not installed" : gpg_strerror (err)); } else { es_fprintf (outfp, "###\n### %s config \"%s\"\n###\n", global? "global":"local", fname); es_fprintf (outfp, CUTLINE_FMT, "start"); err = my_copy_file (fp, outfp, listp); if (err) log_error ("error copying file \"%s\": %s\n", fname, gpg_strerror (err)); es_fprintf (outfp, CUTLINE_FMT, "end--"); es_fclose (fp); } } #ifdef HAVE_W32_SYSTEM /* Print registry entries relevant to the GnuPG system and related * software. */ static void show_other_registry_entries (estream_t outfp) { static struct { int group; const char *name; } names[] = { { 1, "HKLM\\Software\\Gpg4win:Install Directory" }, { 1, "HKLM\\Software\\Gpg4win:Desktop-Version" }, { 1, "HKLM\\Software\\Gpg4win:VS-Desktop-Version" }, { 1, "\\" GNUPG_REGISTRY_DIR ":HomeDir" }, { 1, "\\" GNUPG_REGISTRY_DIR ":DefaultLogFile" }, { 2, "\\Software\\Microsoft\\Office\\Outlook\\Addins\\GNU.GpgOL" ":LoadBehavior" }, { 2, "HKCU\\Software\\Microsoft\\Office\\16.0\\Outlook\\Options\\Mail:" "ReadAsPlain" }, { 2, "HKCU\\Software\\Policies\\Microsoft\\Office\\16.0\\Outlook\\" "Options\\Mail:ReadAsPlain" }, { 3, "logFile" }, { 3, "enableDebug" }, { 3, "searchSmimeServers" }, { 3, "smimeInsecureReplyAllowed" }, { 3, "enableSmime" }, { 3, "preferSmime" }, { 3, "encryptDefault" }, { 3, "signDefault" }, { 3, "inlinePGP" }, { 3, "replyCrypt" }, { 3, "autoresolve" }, { 3, "autoretrieve" }, { 3, "automation" }, { 3, "autosecure" }, { 3, "autotrust" }, { 3, "autoencryptUntrusted" }, { 3, "autoimport" }, { 3, "splitBCCMails" }, { 3, "combinedOpsEnabled" }, { 3, "encryptSubject" }, { 0, NULL } }; int idx; int group = 0; char *namebuf = NULL; const char *name; int from_hklm; for (idx=0; (name = names[idx].name); idx++) { char *value; if (names[idx].group == 3) { xfree (namebuf); namebuf = xstrconcat ("\\Software\\GNU\\GpgOL", ":", names[idx].name, NULL); name = namebuf; } value = read_w32_reg_string (name, &from_hklm); if (!value) continue; if (names[idx].group != group) { group = names[idx].group; es_fprintf (outfp, "###\n### %s related:\n", group == 1 ? "GnuPG Desktop" : group == 2 ? "Outlook" : group == 3 ? "\\Software\\GNU\\GpgOL" : "System" ); } if (group == 3) es_fprintf (outfp, "### %s=%s%s\n", names[idx].name, value, from_hklm? " [hklm]":""); else es_fprintf (outfp, "### %s\n### ->%s<-%s\n", name, value, from_hklm? " [hklm]":""); xfree (value); } es_fprintf (outfp, "###\n"); xfree (namebuf); } /* Print registry entries take from a configuration file. */ static void show_registry_entries_from_file (estream_t outfp) { gpg_error_t err; char *fname; estream_t fp; char *line = NULL; size_t length_of_line = 0; size_t maxlen; ssize_t len; char *value = NULL; int from_hklm; int any = 0; fname = make_filename (gnupg_datadir (), "gpgconf.rnames", NULL); fp = es_fopen (fname, "r"); if (!fp) { err = gpg_error_from_syserror (); if (gpg_err_code (err) != GPG_ERR_ENOENT) log_error ("error opening '%s': %s\n", fname, gpg_strerror (err)); goto leave; } maxlen = 2048; /* Set limit. */ while ((len = es_read_line (fp, &line, &length_of_line, &maxlen)) > 0) { if (!maxlen) { err = gpg_error (GPG_ERR_LINE_TOO_LONG); log_error ("error reading '%s': %s\n", fname, gpg_strerror (err)); goto leave; } trim_spaces (line); if (*line == '#') continue; xfree (value); value = read_w32_reg_string (line, &from_hklm); if (!value) continue; if (!any) { any = 1; es_fprintf (outfp, "### Taken from gpgconf.rnames:\n"); } es_fprintf (outfp, "### %s\n### ->%s<-%s\n", line, value, from_hklm? " [hklm]":""); } if (len < 0 || es_ferror (fp)) { err = gpg_error_from_syserror (); log_error ("error reading '%s': %s\n", fname, gpg_strerror (err)); } leave: if (any) es_fprintf (outfp, "###\n"); xfree (value); xfree (line); es_fclose (fp); xfree (fname); } #endif /*HAVE_W32_SYSTEM*/ /* Show all config files. */ static void show_configs (estream_t outfp) { static const char *names[] = { "common.conf", "gpg-agent.conf", "scdaemon.conf", "dirmngr.conf", "gpg.conf", "gpgsm.conf" }; static const char *envvars[] = { "PATH", "http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "LD_LIBRARY_PATH", "LD_PRELOAD", "LD_AUDIT", "LD_ORIGIN_PATH" }; gpg_error_t err; int idx; char *fname; gnupg_dir_t dir; gnupg_dirent_t dir_entry; size_t n; int any; strlist_t list = NULL; strlist_t sl; const char *s; int got_gpgconfconf = 0; es_fprintf (outfp, "### Dump of all standard config files\n"); show_version_gnupg (outfp, "### "); es_fprintf (outfp, "### Libgcrypt %s\n", gcry_check_version (NULL)); es_fprintf (outfp, "### GpgRT %s\n", gpg_error_check_version (NULL)); #ifdef HAVE_W32_SYSTEM es_fprintf (outfp, "### Codepages:"); if (GetConsoleCP () != GetConsoleOutputCP ()) es_fprintf (outfp, " %u/%u", GetConsoleCP (), GetConsoleOutputCP ()); else es_fprintf (outfp, " %u", GetConsoleCP ()); es_fprintf (outfp, " %u", GetACP ()); es_fprintf (outfp, " %u\n", GetOEMCP ()); #endif es_fprintf (outfp, "###\n\n"); list_dirs (outfp, NULL, 1); es_fprintf (outfp, "\n"); for (idx=0; idx < DIM(envvars); idx++) if ((s = getenv (envvars[idx]))) es_fprintf (outfp, "%s=%s\n", envvars[idx], s); es_fprintf (outfp, "\n"); fname = make_filename (gnupg_sysconfdir (), "gpgconf.conf", NULL); if (!gnupg_access (fname, F_OK)) { got_gpgconfconf = 1; show_configs_one_file (fname, 1, outfp, &list); es_fprintf (outfp, "\n"); } xfree (fname); for (idx = 0; idx < DIM (names); idx++) { fname = make_filename (gnupg_sysconfdir (), names[idx], NULL); show_configs_one_file (fname, 1, outfp, &list); xfree (fname); fname = make_filename (gnupg_homedir (), names[idx], NULL); show_configs_one_file (fname, 0, outfp, &list); xfree (fname); es_fprintf (outfp, "\n"); } /* Print the encountered registry values and envvars. */ if (list) { any = 0; for (sl = list; sl; sl = sl->next) if (!(sl->flags & 1)) { if (!any) { any = 1; es_fprintf (outfp, "###\n" "### List of encountered environment variables:\n"); } if ((s = getenv (sl->d))) es_fprintf (outfp, "### %-12s ->%s<-\n", sl->d, s); else es_fprintf (outfp, "### %-12s [not set]\n", sl->d); } if (any) es_fprintf (outfp, "###\n"); } #ifdef HAVE_W32_SYSTEM es_fprintf (outfp, "###\n### Registry entries:\n"); any = 0; if (list) { for (sl = list; sl; sl = sl->next) if ((sl->flags & 1)) { char *p; int from_hklm; if (!any) { any = 1; es_fprintf (outfp, "###\n### Encountered in config files:\n"); } if ((p = read_w32_reg_string (sl->d, &from_hklm))) es_fprintf (outfp, "### %s ->%s<-%s\n", sl->d, p, from_hklm? " [hklm]":""); else es_fprintf (outfp, "### %s [not set]\n", sl->d); xfree (p); } } if (!any) es_fprintf (outfp, "###\n"); show_other_registry_entries (outfp); show_registry_entries_from_file (outfp); #endif /*HAVE_W32_SYSTEM*/ free_strlist (list); any = 0; /* Additional warning. */ if (got_gpgconfconf) { es_fprintf (outfp, "###\n" "### Warning: legacy config file \"gpgconf.conf\" found\n"); any = 1; } /* Check for uncommon files in the home directory. */ dir = gnupg_opendir (gnupg_homedir ()); if (!dir) { err = gpg_error_from_syserror (); log_error ("error reading directory \"%s\": %s\n", gnupg_homedir (), gpg_strerror (err)); return; } while ((dir_entry = gnupg_readdir (dir))) { for (idx = 0; idx < DIM (names); idx++) { n = strlen (names[idx]); if (!ascii_strncasecmp (dir_entry->d_name, names[idx], n) && dir_entry->d_name[n] == '-' && ascii_strncasecmp (dir_entry->d_name, "gpg.conf-1", 10)) { if (!any) { any = 1; es_fprintf (outfp, "###\n" "### Warning: suspicious files in \"%s\":\n", gnupg_homedir ()); } es_fprintf (outfp, "### %s\n", dir_entry->d_name); } } } if (any) es_fprintf (outfp, "###\n"); gnupg_closedir (dir); } diff --git a/tools/gpgtar-create.c b/tools/gpgtar-create.c index 26d37a332..e6f5b55a2 100644 --- a/tools/gpgtar-create.c +++ b/tools/gpgtar-create.c @@ -1,1386 +1,1389 @@ /* gpgtar-create.c - Create a TAR archive * Copyright (C) 2016-2017, 2019-2023 g10 Code GmbH * Copyright (C) 2010, 2012, 2013 Werner Koch * Copyright (C) 2010 Free Software Foundation, Inc. * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include #include #include #include #include #include #include #ifdef HAVE_W32_SYSTEM # define WIN32_LEAN_AND_MEAN # include #else /*!HAVE_W32_SYSTEM*/ # include # include #endif /*!HAVE_W32_SYSTEM*/ #include "../common/i18n.h" #include #include "../common/exechelp.h" #include "../common/sysutils.h" #include "../common/ccparray.h" #include "../common/membuf.h" #include "gpgtar.h" #ifndef HAVE_LSTAT #define lstat(a,b) gnupg_stat ((a), (b)) #endif /* Number of files to be write. */ static unsigned long global_total_files; /* Count the number of written file and thus headers. Extended * headers are not counted. */ static unsigned long global_written_files; /* Total data expected to be written. */ static unsigned long long global_total_data; /* Number of data bytes written so far. */ static unsigned long long global_written_data; /* Object to control the file scanning. */ struct scanctrl_s; typedef struct scanctrl_s *scanctrl_t; struct scanctrl_s { tar_header_t flist; tar_header_t *flist_tail; unsigned long file_count; int nestlevel; }; /* See ../g10/progress.c:write_status_progress for some background. */ static void write_progress (int countmode, unsigned long long current, unsigned long long total_arg) { char units[] = "BKMGTPEZY?"; int unitidx = 0; uint64_t total = total_arg; if (!opt.status_stream) return; /* Not enabled. */ if (countmode) { if (total && current > total) current = total; } else if (total) /* Size mode: This may use units. */ { if (current > total) current = total; while (total > 1024*1024) { total /= 1024; current /= 1024; unitidx++; } } else /* Size mode */ { while (current > 1024*1024) { current /= 1024; unitidx++; } } if (unitidx > sizeof units - 1) unitidx = sizeof units - 1; if (countmode) es_fprintf (opt.status_stream, "[GNUPG:] PROGRESS gpgtar c %zu %zu\n", (size_t)current, (size_t)total); else es_fprintf (opt.status_stream, "[GNUPG:] PROGRESS gpgtar s %zu %zu %c%s\n", (size_t)current, (size_t)total, units[unitidx], unitidx? "iB" : ""); } /* On Windows convert name to UTF8 and return it; caller must release * the result. On Unix or if ALREADY_UTF8 is set, this function is a * mere xtrystrcopy. On failure NULL is returned and ERRNO set. */ static char * name_to_utf8 (const char *name, int already_utf8) { #ifdef HAVE_W32_SYSTEM wchar_t *wstring; char *result; if (already_utf8) result = xtrystrdup (name); else { wstring = native_to_wchar (name); if (!wstring) return NULL; result = wchar_to_utf8 (wstring); xfree (wstring); } return result; #else /*!HAVE_W32_SYSTEM */ (void)already_utf8; return xtrystrdup (name); #endif /*!HAVE_W32_SYSTEM */ } /* Given a fresh header object HDR with only the name field set, try to gather all available info. This is the W32 version. */ #ifdef HAVE_W32_SYSTEM static gpg_error_t fillup_entry_w32 (tar_header_t hdr) { char *p; wchar_t *wfname; WIN32_FILE_ATTRIBUTE_DATA fad; DWORD attr; for (p=hdr->name; *p; p++) if (*p == '/') *p = '\\'; wfname = gpgrt_fname_to_wchar (hdr->name); for (p=hdr->name; *p; p++) if (*p == '\\') *p = '/'; if (!wfname) { log_error ("error converting '%s': %s\n", hdr->name, w32_strerror (-1)); return gpg_error_from_syserror (); } if (!GetFileAttributesExW (wfname, GetFileExInfoStandard, &fad)) { log_error ("error stat-ing '%s': %s\n", hdr->name, w32_strerror (-1)); xfree (wfname); return gpg_error_from_syserror (); } xfree (wfname); attr = fad.dwFileAttributes; if ((attr & FILE_ATTRIBUTE_NORMAL)) hdr->typeflag = TF_REGULAR; else if ((attr & FILE_ATTRIBUTE_DIRECTORY)) hdr->typeflag = TF_DIRECTORY; else if ((attr & FILE_ATTRIBUTE_DEVICE)) hdr->typeflag = TF_NOTSUP; else if ((attr & (FILE_ATTRIBUTE_OFFLINE | FILE_ATTRIBUTE_TEMPORARY))) hdr->typeflag = TF_NOTSUP; else hdr->typeflag = TF_REGULAR; /* Map some attributes to USTAR defined mode bits. */ hdr->mode = 0640; /* User may read and write, group only read. */ if ((attr & FILE_ATTRIBUTE_DIRECTORY)) hdr->mode |= 0110; /* Dirs are user and group executable. */ if ((attr & FILE_ATTRIBUTE_READONLY)) hdr->mode &= ~0200; /* Clear the user write bit. */ if ((attr & FILE_ATTRIBUTE_HIDDEN)) hdr->mode &= ~0707; /* Clear all user and other bits. */ if ((attr & FILE_ATTRIBUTE_SYSTEM)) hdr->mode |= 0004; /* Make it readable by other. */ /* Only set the size for a regular file. */ if (hdr->typeflag == TF_REGULAR) hdr->size = (fad.nFileSizeHigh * ((unsigned long long)MAXDWORD+1) + fad.nFileSizeLow); hdr->mtime = (((unsigned long long)fad.ftLastWriteTime.dwHighDateTime << 32) | fad.ftLastWriteTime.dwLowDateTime); if (!hdr->mtime) hdr->mtime = (((unsigned long long)fad.ftCreationTime.dwHighDateTime << 32) | fad.ftCreationTime.dwLowDateTime); hdr->mtime -= 116444736000000000ULL; /* The filetime epoch is 1601-01-01. */ hdr->mtime /= 10000000; /* Convert from 0.1us to seconds. */ return 0; } #endif /*HAVE_W32_SYSTEM*/ /* Given a fresh header object HDR with only the name field set, try to gather all available info. This is the POSIX version. */ #ifndef HAVE_W32_SYSTEM static gpg_error_t fillup_entry_posix (tar_header_t hdr) { gpg_error_t err; struct stat sbuf; if (lstat (hdr->name, &sbuf)) { err = gpg_error_from_syserror (); log_error ("error stat-ing '%s': %s\n", hdr->name, gpg_strerror (err)); return err; } if (S_ISREG (sbuf.st_mode)) hdr->typeflag = TF_REGULAR; else if (S_ISDIR (sbuf.st_mode)) hdr->typeflag = TF_DIRECTORY; else if (S_ISCHR (sbuf.st_mode)) hdr->typeflag = TF_CHARDEV; else if (S_ISBLK (sbuf.st_mode)) hdr->typeflag = TF_BLOCKDEV; else if (S_ISFIFO (sbuf.st_mode)) hdr->typeflag = TF_FIFO; else if (S_ISLNK (sbuf.st_mode)) hdr->typeflag = TF_SYMLINK; else hdr->typeflag = TF_NOTSUP; /* FIXME: Save DEV and INO? */ /* Set the USTAR defined mode bits using the system macros. */ if (sbuf.st_mode & S_IRUSR) hdr->mode |= 0400; if (sbuf.st_mode & S_IWUSR) hdr->mode |= 0200; if (sbuf.st_mode & S_IXUSR) hdr->mode |= 0100; if (sbuf.st_mode & S_IRGRP) hdr->mode |= 0040; if (sbuf.st_mode & S_IWGRP) hdr->mode |= 0020; if (sbuf.st_mode & S_IXGRP) hdr->mode |= 0010; if (sbuf.st_mode & S_IROTH) hdr->mode |= 0004; if (sbuf.st_mode & S_IWOTH) hdr->mode |= 0002; if (sbuf.st_mode & S_IXOTH) hdr->mode |= 0001; #ifdef S_IXUID if (sbuf.st_mode & S_IXUID) hdr->mode |= 04000; #endif #ifdef S_IXGID if (sbuf.st_mode & S_IXGID) hdr->mode |= 02000; #endif #ifdef S_ISVTX if (sbuf.st_mode & S_ISVTX) hdr->mode |= 01000; #endif hdr->nlink = sbuf.st_nlink; hdr->uid = sbuf.st_uid; hdr->gid = sbuf.st_gid; /* Only set the size for a regular file. */ if (hdr->typeflag == TF_REGULAR) hdr->size = sbuf.st_size; hdr->mtime = sbuf.st_mtime; return 0; } #endif /*!HAVE_W32_SYSTEM*/ /* Add a new entry. The name of a directory entry is ENTRYNAME; if that is NULL, DNAME is the name of the directory itself. Under Windows ENTRYNAME shall have backslashes replaced by standard slashes. */ static gpg_error_t add_entry (const char *dname, const char *entryname, scanctrl_t scanctrl) { gpg_error_t err; tar_header_t hdr; char *p; size_t dnamelen = strlen (dname); log_assert (dnamelen); hdr = xtrycalloc (1, sizeof *hdr + dnamelen + 1 + (entryname? strlen (entryname) : 0) + 1); if (!hdr) return gpg_error_from_syserror (); p = stpcpy (hdr->name, dname); if (entryname) { if (dname[dnamelen-1] != '/') *p++ = '/'; strcpy (p, entryname); } else { if (hdr->name[dnamelen-1] == '/') hdr->name[dnamelen-1] = 0; } #ifdef HAVE_DOSISH_SYSTEM err = fillup_entry_w32 (hdr); #else err = fillup_entry_posix (hdr); #endif if (err) xfree (hdr); else { /* FIXME: We don't have the extended info yet available so we * can't print them. */ if (opt.verbose) gpgtar_print_header (hdr, NULL, log_get_stream ()); *scanctrl->flist_tail = hdr; scanctrl->flist_tail = &hdr->next; scanctrl->file_count++; /* Print a progress line during scnanning in increments of 5000 * and not of 100 as we doing during write: Scanning is of * course much faster. */ if (!(scanctrl->file_count % 5000)) write_progress (1, scanctrl->file_count, 0); } return 0; } static gpg_error_t scan_directory (const char *dname, scanctrl_t scanctrl) { gpg_error_t err = 0; #ifdef HAVE_W32_SYSTEM /* Note that we introduced gnupg_opendir only after we had deployed * this code and thus we don't change it for now. */ WIN32_FIND_DATAW fi; HANDLE hd = INVALID_HANDLE_VALUE; char *p; if (!*dname) return 0; /* An empty directory name has no entries. */ { char *fname; wchar_t *wfname; fname = xtrymalloc (strlen (dname) + 2 + 2 + 1); if (!fname) { err = gpg_error_from_syserror (); goto leave; } if (!strcmp (dname, "/")) strcpy (fname, "/*"); /* Trailing slash is not allowed. */ else if (!strcmp (dname, ".")) strcpy (fname, "*"); else if (*dname && dname[strlen (dname)-1] == '/') strcpy (stpcpy (fname, dname), "*"); else if (*dname && dname[strlen (dname)-1] != '*') strcpy (stpcpy (fname, dname), "/*"); else strcpy (fname, dname); for (p=fname; *p; p++) if (*p == '/') *p = '\\'; wfname = gpgrt_fname_to_wchar (fname); xfree (fname); if (!wfname) { err = gpg_error_from_syserror (); log_error (_("error reading directory '%s': %s\n"), dname, gpg_strerror (err)); goto leave; } hd = FindFirstFileW (wfname, &fi); if (hd == INVALID_HANDLE_VALUE) { err = gpg_error_from_syserror (); log_error (_("error reading directory '%s': %s\n"), dname, w32_strerror (-1)); xfree (wfname); goto leave; } xfree (wfname); } do { char *fname = wchar_to_utf8 (fi.cFileName); if (!fname) { err = gpg_error_from_syserror (); log_error ("error converting filename: %s\n", w32_strerror (-1)); break; } for (p=fname; *p; p++) if (*p == '\\') *p = '/'; if (!strcmp (fname, "." ) || !strcmp (fname, "..")) err = 0; /* Skip self and parent dir entry. */ else if (!strncmp (dname, "./", 2) && dname[2]) err = add_entry (dname+2, fname, scanctrl); else err = add_entry (dname, fname, scanctrl); xfree (fname); } while (!err && FindNextFileW (hd, &fi)); if (err) ; else if (GetLastError () == ERROR_NO_MORE_FILES) err = 0; else { err = gpg_error_from_syserror (); log_error (_("error reading directory '%s': %s\n"), dname, w32_strerror (-1)); } leave: if (hd != INVALID_HANDLE_VALUE) FindClose (hd); #else /*!HAVE_W32_SYSTEM*/ DIR *dir; struct dirent *de; if (!*dname) return 0; /* An empty directory name has no entries. */ dir = opendir (dname); if (!dir) { err = gpg_error_from_syserror (); log_error (_("error reading directory '%s': %s\n"), dname, gpg_strerror (err)); return err; } while ((de = readdir (dir))) { if (!strcmp (de->d_name, "." ) || !strcmp (de->d_name, "..")) continue; /* Skip self and parent dir entry. */ err = add_entry (dname, de->d_name, scanctrl); if (err) goto leave; } leave: closedir (dir); #endif /*!HAVE_W32_SYSTEM*/ return err; } static gpg_error_t scan_recursive (const char *dname, scanctrl_t scanctrl) { gpg_error_t err = 0; tar_header_t hdr, *start_tail, *stop_tail; if (scanctrl->nestlevel > 200) { log_error ("directories too deeply nested\n"); return gpg_error (GPG_ERR_RESOURCE_LIMIT); } scanctrl->nestlevel++; log_assert (scanctrl->flist_tail); start_tail = scanctrl->flist_tail; scan_directory (dname, scanctrl); stop_tail = scanctrl->flist_tail; hdr = *start_tail; for (; hdr && hdr != *stop_tail; hdr = hdr->next) if (hdr->typeflag == TF_DIRECTORY) { if (opt.verbose > 1) log_info ("scanning directory '%s'\n", hdr->name); scan_recursive (hdr->name, scanctrl); } scanctrl->nestlevel--; return err; } /* Returns true if PATTERN is acceptable. */ static int pattern_valid_p (const char *pattern) { if (!*pattern) return 0; if (*pattern == '.' && pattern[1] == '.') return 0; if (*pattern == '/' #ifdef HAVE_DOSISH_SYSTEM || *pattern == '\\' #endif ) return 0; /* Absolute filenames are not supported. */ #ifdef HAVE_DRIVE_LETTERS if (((*pattern >= 'a' && *pattern <= 'z') || (*pattern >= 'A' && *pattern <= 'Z')) && pattern[1] == ':') return 0; /* Drive letter are not allowed either. */ #endif /*HAVE_DRIVE_LETTERS*/ return 1; /* Okay. */ } static void store_xoctal (char *buffer, size_t length, unsigned long long value) { char *p, *pend; size_t n; unsigned long long v; log_assert (length > 1); v = value; n = length; p = pend = buffer + length; *--p = 0; /* Nul byte. */ n--; do { *--p = '0' + (v % 8); v /= 8; n--; } while (v && n); if (!v) { /* Pad. */ for ( ; n; n--) *--p = '0'; } else /* Does not fit into the field. Store as binary number. */ { v = value; n = length; p = pend = buffer + length; do { *--p = v; v /= 256; n--; } while (v && n); if (!v) { /* Pad. */ for ( ; n; n--) *--p = 0; if (*p & 0x80) BUG (); *p |= 0x80; /* Set binary flag. */ } else BUG (); } } static void store_uname (char *buffer, size_t length, unsigned long uid) { static int initialized; static unsigned long lastuid; static char lastuname[32]; if (!initialized || uid != lastuid) { #ifdef HAVE_W32_SYSTEM mem2str (lastuname, uid? "user":"root", sizeof lastuname); #else struct passwd *pw = getpwuid (uid); lastuid = uid; initialized = 1; if (pw) mem2str (lastuname, pw->pw_name, sizeof lastuname); else { log_info ("failed to get name for uid %lu\n", uid); *lastuname = 0; } #endif } mem2str (buffer, lastuname, length); } static void store_gname (char *buffer, size_t length, unsigned long gid) { static int initialized; static unsigned long lastgid; static char lastgname[32]; if (!initialized || gid != lastgid) { #ifdef HAVE_W32_SYSTEM mem2str (lastgname, gid? "users":"root", sizeof lastgname); #else struct group *gr = getgrgid (gid); lastgid = gid; initialized = 1; if (gr) mem2str (lastgname, gr->gr_name, sizeof lastgname); else { log_info ("failed to get name for gid %lu\n", gid); *lastgname = 0; } #endif } mem2str (buffer, lastgname, length); } static void compute_checksum (void *record) { struct ustar_raw_header *raw = record; unsigned long chksum = 0; unsigned char *p; size_t n; memset (raw->checksum, ' ', sizeof raw->checksum); p = record; for (n=0; n < RECORDSIZE; n++) chksum += *p++; store_xoctal (raw->checksum, sizeof raw->checksum - 1, chksum); raw->checksum[7] = ' '; } /* Read a symlink without truncating it. Caller must release the * returned buffer. Returns NULL on error. */ #ifndef HAVE_W32_SYSTEM static char * myreadlink (const char *name) { char *buffer; size_t size; int nread; for (size = 1024; size <= 65536; size *= 2) { buffer = xtrymalloc (size); if (!buffer) return NULL; nread = readlink (name, buffer, size - 1); if (nread < 0) { xfree (buffer); return NULL; } if (nread < size - 1) { buffer[nread] = 0; return buffer; /* Got it. */ } xfree (buffer); } gpg_err_set_errno (ERANGE); return NULL; } #endif /*Unix*/ /* Build a header. If the filename or the link name ist too long * allocate an exthdr and use a replacement file name in RECORD. * Caller should always release R_EXTHDR; this function initializes it * to point to NULL. */ static gpg_error_t build_header (void *record, tar_header_t hdr, strlist_t *r_exthdr) { gpg_error_t err; struct ustar_raw_header *raw = record; size_t namelen, n; strlist_t sl; memset (record, 0, RECORDSIZE); *r_exthdr = NULL; /* Store name and prefix. */ namelen = strlen (hdr->name); if (namelen < sizeof raw->name) memcpy (raw->name, hdr->name, namelen); else { n = (namelen < sizeof raw->prefix)? namelen : sizeof raw->prefix; for (n--; n ; n--) if (hdr->name[n] == '/') break; if (namelen - n < sizeof raw->name) { /* Note that the N is < sizeof prefix and that the delimiting slash is not stored. */ memcpy (raw->prefix, hdr->name, n); memcpy (raw->name, hdr->name+n+1, namelen - n); } else { /* Too long - prepare extended header. */ sl = add_to_strlist_try (r_exthdr, hdr->name); if (!sl) { err = gpg_error_from_syserror (); log_error ("error storing file '%s': %s\n", hdr->name, gpg_strerror (err)); return err; } sl->flags = 1; /* Mark as path */ /* The name we use is not POSIX compliant but because we * expect that (for security issues) a tarball will anyway * be extracted to a unique new directory, a simple counter * will do. To ease testing we also put in the PID. The * count is bumped after the header has been written. */ snprintf (raw->name, sizeof raw->name-1, "_@paxheader.%u.%lu", (unsigned int)getpid(), global_written_files + 1); } } store_xoctal (raw->mode, sizeof raw->mode, hdr->mode); store_xoctal (raw->uid, sizeof raw->uid, hdr->uid); store_xoctal (raw->gid, sizeof raw->gid, hdr->gid); store_xoctal (raw->size, sizeof raw->size, hdr->size); store_xoctal (raw->mtime, sizeof raw->mtime, hdr->mtime); switch (hdr->typeflag) { case TF_REGULAR: raw->typeflag[0] = '0'; break; case TF_HARDLINK: raw->typeflag[0] = '1'; break; case TF_SYMLINK: raw->typeflag[0] = '2'; break; case TF_CHARDEV: raw->typeflag[0] = '3'; break; case TF_BLOCKDEV: raw->typeflag[0] = '4'; break; case TF_DIRECTORY: raw->typeflag[0] = '5'; break; case TF_FIFO: raw->typeflag[0] = '6'; break; default: return gpg_error (GPG_ERR_NOT_SUPPORTED); } memcpy (raw->magic, "ustar", 6); raw->version[0] = '0'; raw->version[1] = '0'; store_uname (raw->uname, sizeof raw->uname, hdr->uid); store_gname (raw->gname, sizeof raw->gname, hdr->gid); #ifndef HAVE_W32_SYSTEM if (hdr->typeflag == TF_SYMLINK) { int nread; char *p; nread = readlink (hdr->name, raw->linkname, sizeof raw->linkname -1); if (nread < 0) { err = gpg_error_from_syserror (); log_error ("error reading symlink '%s': %s\n", hdr->name, gpg_strerror (err)); return err; } raw->linkname[nread] = 0; if (nread == sizeof raw->linkname -1) { /* Truncated - read again and store as extended header. */ p = myreadlink (hdr->name); if (!p) { err = gpg_error_from_syserror (); log_error ("error reading symlink '%s': %s\n", hdr->name, gpg_strerror (err)); return err; } sl = add_to_strlist_try (r_exthdr, p); xfree (p); if (!sl) { err = gpg_error_from_syserror (); log_error ("error storing syslink '%s': %s\n", hdr->name, gpg_strerror (err)); return err; } sl->flags = 2; /* Mark as linkpath */ } } #endif /*!HAVE_W32_SYSTEM*/ compute_checksum (record); return 0; } /* Add an extended header record (NAME,VALUE) to the buffer MB. */ static void add_extended_header_record (membuf_t *mb, const char *name, const char *value) { size_t n, n0, n1; char numbuf[35]; size_t valuelen; /* To avoid looping in most cases, we guess the initial value. */ valuelen = strlen (value); n1 = valuelen > 95? 3 : 2; do { n0 = n1; /* (3 for the space before name, the '=', and the LF.) */ n = n0 + strlen (name) + valuelen + 3; snprintf (numbuf, sizeof numbuf, "%zu", n); n1 = strlen (numbuf); } while (n0 != n1); put_membuf_str (mb, numbuf); put_membuf (mb, " ", 1); put_membuf_str (mb, name); put_membuf (mb, "=", 1); put_membuf (mb, value, valuelen); put_membuf (mb, "\n", 1); } /* Write the extended header specified by EXTHDR to STREAM. */ static gpg_error_t write_extended_header (estream_t stream, const void *record, strlist_t exthdr) { gpg_error_t err = 0; struct ustar_raw_header raw; strlist_t sl; membuf_t mb; char *buffer, *p; size_t buflen; init_membuf (&mb, 2*RECORDSIZE); for (sl=exthdr; sl; sl = sl->next) { if (sl->flags == 1) add_extended_header_record (&mb, "path", sl->d); else if (sl->flags == 2) add_extended_header_record (&mb, "linkpath", sl->d); } buffer = get_membuf (&mb, &buflen); if (!buffer) { err = gpg_error_from_syserror (); log_error ("error building extended header: %s\n", gpg_strerror (err)); goto leave; } /* We copy the header from the standard header record, so that an * extracted extended header (using a non-pax aware software) is * written with the same properties as the original file. The real * entry will overwrite it anyway. Of course we adjust the size and * the type. */ memcpy (&raw, record, RECORDSIZE); store_xoctal (raw.size, sizeof raw.size, buflen); raw.typeflag[0] = 'x'; /* Mark as extended header. */ compute_checksum (&raw); err = write_record (stream, &raw); if (err) goto leave; for (p = buffer; buflen >= RECORDSIZE; p += RECORDSIZE, buflen -= RECORDSIZE) { err = write_record (stream, p); if (err) goto leave; } if (buflen) { /* Reuse RAW for builidng the last record. */ memcpy (&raw, p, buflen); memset ((char*)&raw+buflen, 0, RECORDSIZE - buflen); err = write_record (stream, &raw); if (err) goto leave; } leave: xfree (buffer); return err; } static gpg_error_t write_file (estream_t stream, tar_header_t hdr, unsigned int *skipped_open) { gpg_error_t err; char record[RECORDSIZE]; estream_t infp; size_t nread, nbytes; strlist_t exthdr = NULL; int any; err = build_header (record, hdr, &exthdr); if (err) { if (gpg_err_code (err) == GPG_ERR_NOT_SUPPORTED) { log_info ("silently skipping unsupported file '%s'\n", hdr->name); err = 0; } return err; } if (hdr->typeflag == TF_REGULAR) { infp = es_fopen (hdr->name, "rb,sysopen"); if (!infp) { err = gpg_error_from_syserror (); log_info ("can't open '%s': %s - skipped\n", hdr->name, gpg_strerror (err)); ++*skipped_open; if (!*skipped_open) /* Protect against overflow. */ --*skipped_open; return 0; } } else infp = NULL; if (exthdr && (err = write_extended_header (stream, record, exthdr))) goto leave; err = write_record (stream, record); if (err) goto leave; global_written_files++; if (!(global_written_files % 100)) write_progress (1, global_written_files, global_total_files); if (hdr->typeflag == TF_REGULAR) { hdr->nrecords = (hdr->size + RECORDSIZE-1)/RECORDSIZE; any = 0; while (hdr->nrecords--) { nbytes = hdr->nrecords? RECORDSIZE : (hdr->size % RECORDSIZE); if (!nbytes) nbytes = RECORDSIZE; nread = es_fread (record, 1, nbytes, infp); if (nread != nbytes) { err = gpg_error_from_syserror (); log_error ("error reading file '%s': %s%s\n", hdr->name, gpg_strerror (err), any? " (file shrunk?)":""); goto leave; } else if (nbytes < RECORDSIZE) memset (record + nbytes, 0, RECORDSIZE - nbytes); any = 1; err = write_record (stream, record); if (err) goto leave; global_written_data += nbytes; if (!((global_written_data/nbytes) % (2048*100))) write_progress (0, global_written_data, global_total_data); } nread = es_fread (record, 1, 1, infp); if (nread) log_info ("note: file '%s' has grown\n", hdr->name); } leave: if (err) es_fclose (infp); else if ((err = es_fclose (infp))) log_error ("error closing file '%s': %s\n", hdr->name, gpg_strerror (err)); free_strlist (exthdr); return err; } static gpg_error_t write_eof_mark (estream_t stream) { gpg_error_t err; char record[RECORDSIZE]; memset (record, 0, sizeof record); err = write_record (stream, record); if (!err) err = write_record (stream, record); return err; } /* Create a new tarball using the names in the array INPATTERN. If INPATTERN is NULL take the pattern as null terminated strings from stdin or from the file specified by FILES_FROM. If NULL_NAMES is set the filenames in such a file are delimited by a binary Nul and not by a LF. */ gpg_error_t gpgtar_create (char **inpattern, const char *files_from, int null_names, int encrypt, int sign) { gpg_error_t err = 0; struct scanctrl_s scanctrl_buffer; scanctrl_t scanctrl = &scanctrl_buffer; tar_header_t hdr, *start_tail; estream_t files_from_stream = NULL; estream_t outstream = NULL; int eof_seen = 0; - pid_t pid = (pid_t)(-1); + gnupg_process_t proc = NULL; unsigned int skipped_open = 0; memset (scanctrl, 0, sizeof *scanctrl); scanctrl->flist_tail = &scanctrl->flist; if (!inpattern) { if (!files_from || !strcmp (files_from, "-")) { files_from = "-"; files_from_stream = es_stdin; if (null_names) es_set_binary (es_stdin); } else if (!(files_from_stream=es_fopen (files_from, null_names? "rb":"r"))) { err = gpg_error_from_syserror (); log_error ("error opening '%s': %s\n", files_from, gpg_strerror (err)); return err; } } if (opt.directory && gnupg_chdir (opt.directory)) { err = gpg_error_from_syserror (); log_error ("chdir to '%s' failed: %s\n", opt.directory, gpg_strerror (err)); return err; } while (!eof_seen) { char *pat, *p; int skip_this = 0; if (inpattern) { const char *pattern = *inpattern; if (!pattern) break; /* End of array. */ inpattern++; if (!*pattern) continue; pat = name_to_utf8 (pattern, 0); } else /* Read Nul or LF delimited pattern from files_from_stream. */ { int c; char namebuf[4096]; size_t n = 0; for (;;) { if ((c = es_getc (files_from_stream)) == EOF) { if (es_ferror (files_from_stream)) { err = gpg_error_from_syserror (); log_error ("error reading '%s': %s\n", files_from, gpg_strerror (err)); goto leave; } c = null_names ? 0 : '\n'; eof_seen = 1; } if (n >= sizeof namebuf - 1) { if (!skip_this) { skip_this = 1; log_error ("error reading '%s': %s\n", files_from, "filename too long"); } } else namebuf[n++] = c; if (null_names) { if (!c) { namebuf[n] = 0; break; } } else /* Shall be LF delimited. */ { if (!c) { if (!skip_this) { skip_this = 1; log_error ("error reading '%s': %s\n", files_from, "filename with embedded Nul"); } } else if ( c == '\n' ) { namebuf[n] = 0; ascii_trim_spaces (namebuf); n = strlen (namebuf); break; } } } if (skip_this || n < 2) continue; pat = name_to_utf8 (namebuf, opt.utf8strings); } if (!pat) { err = gpg_error_from_syserror (); log_error ("memory allocation problem: %s\n", gpg_strerror (err)); goto leave; } for (p=pat; *p; p++) if (*p == '\\') *p = '/'; if (opt.verbose > 1) log_info ("scanning '%s'\n", pat); start_tail = scanctrl->flist_tail; if (skip_this || !pattern_valid_p (pat)) log_error ("skipping invalid name '%s'\n", pat); else if (!add_entry (pat, NULL, scanctrl) && *start_tail && ((*start_tail)->typeflag & TF_DIRECTORY)) scan_recursive (pat, scanctrl); xfree (pat); } if (files_from_stream && files_from_stream != es_stdin) es_fclose (files_from_stream); global_total_files = global_total_data = 0; global_written_files = global_written_data = 0; for (hdr = scanctrl->flist; hdr; hdr = hdr->next) { global_total_files++; global_total_data += hdr->size; } write_progress (1, 0, global_total_files); write_progress (0, 0, global_total_data); if (encrypt || sign) { strlist_t arg; ccparray_t ccp; int except[2] = { -1, -1 }; const char **argv; /* '--encrypt' may be combined with '--symmetric', but 'encrypt' * is set either way. Clear it if no recipients are specified. */ if (opt.symmetric && opt.recipients == NULL) encrypt = 0; ccparray_init (&ccp, 0); if (opt.batch) ccparray_put (&ccp, "--batch"); if (opt.answer_yes) ccparray_put (&ccp, "--yes"); if (opt.answer_no) ccparray_put (&ccp, "--no"); if (opt.require_compliance) ccparray_put (&ccp, "--require-compliance"); if (opt.status_fd != -1) { static char tmpbuf[40]; snprintf (tmpbuf, sizeof tmpbuf, "--status-fd=%d", opt.status_fd); ccparray_put (&ccp, tmpbuf); except[0] = opt.status_fd; } ccparray_put (&ccp, "--output"); ccparray_put (&ccp, opt.outfile? opt.outfile : "-"); if (encrypt) ccparray_put (&ccp, "--encrypt"); if (sign) ccparray_put (&ccp, "--sign"); if (opt.user) { ccparray_put (&ccp, "--local-user"); ccparray_put (&ccp, opt.user); } if (opt.symmetric) ccparray_put (&ccp, "--symmetric"); for (arg = opt.recipients; arg; arg = arg->next) { ccparray_put (&ccp, "--recipient"); ccparray_put (&ccp, arg->d); } for (arg = opt.gpg_arguments; arg; arg = arg->next) ccparray_put (&ccp, arg->d); ccparray_put (&ccp, NULL); argv = ccparray_get (&ccp, NULL); if (!argv) { err = gpg_error_from_syserror (); goto leave; } - err = gnupg_spawn_process (opt.gpg_program, argv, - except[0] == -1? NULL : except, - (GNUPG_SPAWN_KEEP_STDOUT - | GNUPG_SPAWN_KEEP_STDERR), - &outstream, NULL, NULL, &pid); + err = gnupg_process_spawn (opt.gpg_program, argv, + (GNUPG_PROCESS_STDIN_PIPE + | GNUPG_PROCESS_STDOUT_KEEP + | GNUPG_PROCESS_STDERR_KEEP), + gnupg_spawn_helper, except, &proc); xfree (argv); if (err) goto leave; + gnupg_process_get_streams (proc, 0, &outstream, NULL, NULL); es_set_binary (outstream); } else if (opt.outfile) /* No crypto */ { if (!strcmp (opt.outfile, "-")) outstream = es_stdout; else outstream = es_fopen (opt.outfile, "wb,sysopen"); if (!outstream) { err = gpg_error_from_syserror (); goto leave; } if (outstream == es_stdout) es_set_binary (es_stdout); } else /* Also no crypto. */ { outstream = es_stdout; es_set_binary (outstream); } skipped_open = 0; for (hdr = scanctrl->flist; hdr; hdr = hdr->next) { err = write_file (outstream, hdr, &skipped_open); if (err) goto leave; } err = write_eof_mark (outstream); if (err) goto leave; write_progress (1, global_written_files, global_total_files); write_progress (0, global_written_data, global_total_data); - if (pid != (pid_t)(-1)) + if (proc) { - int exitcode; - err = es_fclose (outstream); outstream = NULL; if (err) log_error ("error closing pipe: %s\n", gpg_strerror (err)); - else + + err = gnupg_process_wait (proc, 1); + if (!err) { - err = gnupg_wait_process (opt.gpg_program, pid, 1, &exitcode); - if (err) + int exitcode; + + gnupg_process_ctl (proc, GNUPG_PROCESS_GET_EXIT_ID, &exitcode); + if (exitcode) log_error ("running %s failed (exitcode=%d): %s", opt.gpg_program, exitcode, gpg_strerror (err)); - gnupg_release_process (pid); - pid = (pid_t)(-1); } + gnupg_process_release (proc); + proc = NULL; } if (skipped_open) { log_info ("number of skipped files: %u\n", skipped_open); log_error ("exiting with failure status due to previous errors\n"); } leave: if (!err) { gpg_error_t first_err; - if (outstream != es_stdout || pid != (pid_t)(-1)) + if (outstream != es_stdout) first_err = es_fclose (outstream); else first_err = es_fflush (outstream); outstream = NULL; if (! err) err = first_err; } if (err) { log_error ("creating tarball '%s' failed: %s\n", opt.outfile ? opt.outfile : "-", gpg_strerror (err)); if (outstream && outstream != es_stdout) es_fclose (outstream); if (opt.outfile) gnupg_remove (opt.outfile); } scanctrl->flist_tail = NULL; while ( (hdr = scanctrl->flist) ) { scanctrl->flist = hdr->next; xfree (hdr); } return err; } diff --git a/tools/gpgtar-extract.c b/tools/gpgtar-extract.c index 936d03e3e..be483f87c 100644 --- a/tools/gpgtar-extract.c +++ b/tools/gpgtar-extract.c @@ -1,532 +1,534 @@ /* gpgtar-extract.c - Extract from a TAR archive * Copyright (C) 2016-2017, 2019-2022 g10 Code GmbH * Copyright (C) 2010, 2012, 2013 Werner Koch * Copyright (C) 2010 Free Software Foundation, Inc. * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include #include #include #include #include #include #include "../common/i18n.h" #include #include "../common/exechelp.h" #include "../common/sysutils.h" #include "../common/ccparray.h" #include "gpgtar.h" static gpg_error_t check_suspicious_name (const char *name, tarinfo_t info) { size_t n; n = strlen (name); #ifdef HAVE_DOSISH_SYSTEM if (strchr (name, '\\')) { log_error ("filename '%s' contains a backslash - " "can't extract on this system\n", name); info->skipped_badname++; return gpg_error (GPG_ERR_INV_NAME); } #endif /*HAVE_DOSISH_SYSTEM*/ if (!n || strstr (name, "//") || strstr (name, "/../") || !strncmp (name, "../", 3) || (n >= 3 && !strcmp (name+n-3, "/.." ))) { log_error ("filename '%s' has suspicious parts - not extracting\n", name); info->skipped_suspicious++; return gpg_error (GPG_ERR_INV_NAME); } return 0; } static gpg_error_t extract_regular (estream_t stream, const char *dirname, tarinfo_t info, tar_header_t hdr, strlist_t exthdr) { gpg_error_t err; char record[RECORDSIZE]; size_t n, nbytes, nwritten; char *fname_buffer = NULL; const char *fname; estream_t outfp = NULL; strlist_t sl; fname = hdr->name; for (sl = exthdr; sl; sl = sl->next) if (sl->flags == 1) fname = sl->d; err = check_suspicious_name (fname, info); if (err) goto leave; fname_buffer = strconcat (dirname, "/", fname, NULL); if (!fname_buffer) { err = gpg_error_from_syserror (); log_error ("error creating filename: %s\n", gpg_strerror (err)); goto leave; } fname = fname_buffer; if (opt.dry_run) outfp = es_fopen ("/dev/null", "wb"); else outfp = es_fopen (fname, "wb,sysopen"); if (!outfp) { err = gpg_error_from_syserror (); log_error ("error creating '%s': %s\n", fname, gpg_strerror (err)); goto leave; } for (n=0; n < hdr->nrecords;) { err = read_record (stream, record); if (err) goto leave; info->nblocks++; n++; if (n < hdr->nrecords || (hdr->size && !(hdr->size % RECORDSIZE))) nbytes = RECORDSIZE; else nbytes = (hdr->size % RECORDSIZE); nwritten = es_fwrite (record, 1, nbytes, outfp); if (nwritten != nbytes) { err = gpg_error_from_syserror (); log_error ("error writing '%s': %s\n", fname, gpg_strerror (err)); goto leave; } } /* Fixme: Set permissions etc. */ leave: if (!err) { if (opt.verbose) log_info ("extracted '%s'\n", fname); info->nextracted++; } es_fclose (outfp); if (err && fname && outfp) { if (gnupg_remove (fname)) log_error ("error removing incomplete file '%s': %s\n", fname, gpg_strerror (gpg_error_from_syserror ())); } xfree (fname_buffer); return err; } static gpg_error_t extract_directory (const char *dirname, tarinfo_t info, tar_header_t hdr, strlist_t exthdr) { gpg_error_t err; const char *name; char *fname = NULL; strlist_t sl; name = hdr->name; for (sl = exthdr; sl; sl = sl->next) if (sl->flags == 1) name = sl->d; err = check_suspicious_name (name, info); if (err) goto leave; fname = strconcat (dirname, "/", name, NULL); if (!fname) { err = gpg_error_from_syserror (); log_error ("error creating filename: %s\n", gpg_strerror (err)); goto leave; } /* Remove a possible trailing slash. */ if (fname[strlen (fname)-1] == '/') fname[strlen (fname)-1] = 0; if (! opt.dry_run && gnupg_mkdir (fname, "-rwx------")) { err = gpg_error_from_syserror (); if (gpg_err_code (err) == GPG_ERR_EEXIST) { /* Ignore existing directories while extracting. */ err = 0; } if (gpg_err_code (err) == GPG_ERR_ENOENT) { /* Try to create the directory with parents but keep the original error code in case of a failure. */ int rc = 0; char *p; size_t prefixlen; /* (PREFIXLEN is the length of the new directory we use to * extract the tarball.) */ prefixlen = strlen (dirname) + 1; for (p = fname+prefixlen; (p = strchr (p, '/')); p++) { *p = 0; rc = gnupg_mkdir (fname, "-rwx------"); if (gpg_err_code (rc) == GPG_ERR_EEXIST) rc = 0; *p = '/'; if (rc) break; } if (!rc && !gnupg_mkdir (fname, "-rwx------")) err = 0; } if (err) log_error ("error creating directory '%s': %s\n", fname, gpg_strerror (err)); } leave: if (!err && opt.verbose) log_info ("created '%s/'\n", fname); xfree (fname); return err; } static gpg_error_t extract (estream_t stream, const char *dirname, tarinfo_t info, tar_header_t hdr, strlist_t exthdr) { gpg_error_t err; size_t n; if (hdr->typeflag == TF_REGULAR || hdr->typeflag == TF_UNKNOWN) err = extract_regular (stream, dirname, info, hdr, exthdr); else if (hdr->typeflag == TF_DIRECTORY) err = extract_directory (dirname, info, hdr, exthdr); else { char record[RECORDSIZE]; log_info ("unsupported file type %d for '%s' - skipped\n", (int)hdr->typeflag, hdr->name); if (hdr->typeflag == TF_SYMLINK) info->skipped_symlinks++; else if (hdr->typeflag == TF_HARDLINK) info->skipped_hardlinks++; else info->skipped_other++; for (err = 0, n=0; !err && n < hdr->nrecords; n++) { err = read_record (stream, record); if (!err) info->nblocks++; } } return err; } /* Create a new directory to be used for extracting the tarball. Returns the name of the directory which must be freed by the caller. In case of an error a diagnostic is printed and NULL returned. */ static char * create_directory (const char *dirprefix) { gpg_error_t err = 0; char *prefix_buffer = NULL; char *dirname = NULL; size_t n; int idx; /* Remove common suffixes. */ n = strlen (dirprefix); if (n > 4 && (!compare_filenames (dirprefix + n - 4, EXTSEP_S GPGEXT_GPG) || !compare_filenames (dirprefix + n - 4, EXTSEP_S "pgp") || !compare_filenames (dirprefix + n - 4, EXTSEP_S "asc") || !compare_filenames (dirprefix + n - 4, EXTSEP_S "pem") || !compare_filenames (dirprefix + n - 4, EXTSEP_S "p7m") || !compare_filenames (dirprefix + n - 4, EXTSEP_S "p7e"))) { prefix_buffer = xtrystrdup (dirprefix); if (!prefix_buffer) { err = gpg_error_from_syserror (); goto leave; } prefix_buffer[n-4] = 0; dirprefix = prefix_buffer; } for (idx=1; idx < 5000; idx++) { xfree (dirname); dirname = xtryasprintf ("%s_%d_", dirprefix, idx); if (!dirname) { err = gpg_error_from_syserror (); goto leave; } if (!gnupg_mkdir (dirname, "-rwx------")) goto leave; /* Ready. */ if (errno != EEXIST && errno != ENOTDIR) { err = gpg_error_from_syserror (); goto leave; } } err = gpg_error (GPG_ERR_LIMIT_REACHED); leave: if (err) { log_error ("error creating an extract directory: %s\n", gpg_strerror (err)); xfree (dirname); dirname = NULL; } xfree (prefix_buffer); return dirname; } gpg_error_t gpgtar_extract (const char *filename, int decrypt) { gpg_error_t err; estream_t stream = NULL; tar_header_t header = NULL; strlist_t extheader = NULL; const char *dirprefix = NULL; char *dirname = NULL; struct tarinfo_s tarinfo_buffer; tarinfo_t tarinfo = &tarinfo_buffer; - pid_t pid = (pid_t)(-1); + gnupg_process_t proc; char *logfilename = NULL; unsigned long long notextracted; memset (&tarinfo_buffer, 0, sizeof tarinfo_buffer); if (opt.directory) dirname = xtrystrdup (opt.directory); else { if (opt.filename) { dirprefix = strrchr (opt.filename, '/'); if (dirprefix) dirprefix++; else dirprefix = opt.filename; } else if (filename) { dirprefix = strrchr (filename, '/'); if (dirprefix) dirprefix++; else dirprefix = filename; } if (!dirprefix || !*dirprefix) dirprefix = "GPGARCH"; dirname = create_directory (dirprefix); if (!dirname) { err = gpg_error (GPG_ERR_GENERAL); goto leave; } } if (opt.verbose) log_info ("extracting to '%s/'\n", dirname); if (decrypt) { strlist_t arg; ccparray_t ccp; int except[2] = { -1, -1 }; const char **argv; ccparray_init (&ccp, 0); if (opt.batch) ccparray_put (&ccp, "--batch"); if (opt.require_compliance) ccparray_put (&ccp, "--require-compliance"); if (opt.status_fd != -1) { static char tmpbuf[40]; snprintf (tmpbuf, sizeof tmpbuf, "--status-fd=%d", opt.status_fd); ccparray_put (&ccp, tmpbuf); except[0] = opt.status_fd; } if (opt.with_log) { ccparray_put (&ccp, "--log-file"); logfilename = xstrconcat (dirname, ".log", NULL); ccparray_put (&ccp, logfilename); } ccparray_put (&ccp, "--output"); ccparray_put (&ccp, "-"); ccparray_put (&ccp, "--decrypt"); for (arg = opt.gpg_arguments; arg; arg = arg->next) ccparray_put (&ccp, arg->d); if (filename) { ccparray_put (&ccp, "--"); ccparray_put (&ccp, filename); } ccparray_put (&ccp, NULL); argv = ccparray_get (&ccp, NULL); if (!argv) { err = gpg_error_from_syserror (); goto leave; } - err = gnupg_spawn_process (opt.gpg_program, argv, - except[0] == -1? NULL : except, - ((filename? 0 : GNUPG_SPAWN_KEEP_STDIN) - | GNUPG_SPAWN_KEEP_STDERR), - NULL, &stream, NULL, &pid); + err = gnupg_process_spawn (opt.gpg_program, argv, + ((filename ? 0 : GNUPG_PROCESS_STDIN_KEEP) + | GNUPG_PROCESS_STDOUT_PIPE), + gnupg_spawn_helper, except, &proc); xfree (argv); if (err) goto leave; + gnupg_process_get_streams (proc, 0, NULL, &stream, NULL); es_set_binary (stream); } else if (filename) { if (!strcmp (filename, "-")) stream = es_stdin; else stream = es_fopen (filename, "rb,sysopen"); if (!stream) { err = gpg_error_from_syserror (); log_error ("error opening '%s': %s\n", filename, gpg_strerror (err)); return err; } if (stream == es_stdin) es_set_binary (es_stdin); } else { stream = es_stdin; es_set_binary (es_stdin); } for (;;) { err = gpgtar_read_header (stream, tarinfo, &header, &extheader); if (err || header == NULL) goto leave; err = extract (stream, dirname, tarinfo, header, extheader); if (err) goto leave; free_strlist (extheader); extheader = NULL; xfree (header); header = NULL; } - if (pid != (pid_t)(-1)) + if (proc) { - int exitcode; - err = es_fclose (stream); stream = NULL; if (err) log_error ("error closing pipe: %s\n", gpg_strerror (err)); - else + + err = gnupg_process_wait (proc, 1); + if (!err) { - err = gnupg_wait_process (opt.gpg_program, pid, 1, &exitcode); - if (err) + int exitcode; + + gnupg_process_ctl (proc, GNUPG_PROCESS_GET_EXIT_ID, &exitcode); + if (exitcode) log_error ("running %s failed (exitcode=%d): %s", opt.gpg_program, exitcode, gpg_strerror (err)); - gnupg_release_process (pid); - pid = (pid_t)(-1); } + gnupg_process_release (proc); + proc = NULL; } leave: notextracted = tarinfo->skipped_badname; notextracted += tarinfo->skipped_suspicious; notextracted += tarinfo->skipped_symlinks; notextracted += tarinfo->skipped_hardlinks; notextracted += tarinfo->skipped_other; if (opt.status_stream) es_fprintf (opt.status_stream, "[GNUPG:] GPGTAR_EXTRACT" " %llu %llu %lu %lu %lu %lu %lu\n", tarinfo->nextracted, notextracted, tarinfo->skipped_badname, tarinfo->skipped_suspicious, tarinfo->skipped_symlinks, tarinfo->skipped_hardlinks, tarinfo->skipped_other); if (notextracted && !opt.quiet) { log_info ("Number of files not extracted: %llu\n", notextracted); if (tarinfo->skipped_badname) log_info (" invalid name: %lu\n", tarinfo->skipped_badname); if (tarinfo->skipped_suspicious) log_info (" suspicious name: %lu\n", tarinfo->skipped_suspicious); if (tarinfo->skipped_symlinks) log_info (" symlink: %lu\n", tarinfo->skipped_symlinks); if (tarinfo->skipped_hardlinks) log_info (" hardlink: %lu\n", tarinfo->skipped_hardlinks); if (tarinfo->skipped_other) log_info (" other reason: %lu\n", tarinfo->skipped_other); } free_strlist (extheader); xfree (header); xfree (dirname); xfree (logfilename); if (stream != es_stdin) es_fclose (stream); return err; } diff --git a/tools/gpgtar-list.c b/tools/gpgtar-list.c index c5bf25825..31bcd8d46 100644 --- a/tools/gpgtar-list.c +++ b/tools/gpgtar-list.c @@ -1,593 +1,594 @@ /* gpgtar-list.c - List a TAR archive * Copyright (C) 2016-2017, 2019-2022 g10 Code GmbH * Copyright (C) 2010, 2012, 2013 Werner Koch * Copyright (C) 2010 Free Software Foundation, Inc. * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include #include #include #include "../common/i18n.h" #include #include "gpgtar.h" #include "../common/exechelp.h" #include "../common/sysutils.h" #include "../common/ccparray.h" static unsigned long long parse_xoctal (const void *data, size_t length, const char *filename) { const unsigned char *p = data; unsigned long long value; if (!length) value = 0; else if ( (*p & 0x80)) { /* Binary format. */ value = (*p++ & 0x7f); while (--length) { value <<= 8; value |= *p++; } } else { /* Octal format */ value = 0; /* Skip leading spaces and zeroes. */ for (; length && (*p == ' ' || *p == '0'); length--, p++) ; for (; length && *p; length--, p++) { if (*p >= '0' && *p <= '7') { value <<= 3; value += (*p - '0'); } else { log_error ("%s: invalid octal number encountered - assuming 0\n", filename); value = 0; break; } } } return value; } static tar_header_t parse_header (const void *record, const char *filename, tarinfo_t info) { const struct ustar_raw_header *raw = record; size_t n, namelen, prefixlen; tar_header_t header; int use_prefix; int anyerror = 0; info->headerblock = info->nblocks - 1; use_prefix = (!memcmp (raw->magic, "ustar", 5) && (raw->magic[5] == ' ' || !raw->magic[5])); for (namelen=0; namelen < sizeof raw->name && raw->name[namelen]; namelen++) ; if (namelen == sizeof raw->name) { log_info ("%s: warning: name not terminated by a nul\n", filename); anyerror = 1; } for (n=namelen+1; n < sizeof raw->name; n++) if (raw->name[n]) { log_info ("%s: warning: garbage after name\n", filename); anyerror = 1; break; } if (use_prefix && raw->prefix[0]) { for (prefixlen=0; (prefixlen < sizeof raw->prefix && raw->prefix[prefixlen]); prefixlen++) ; if (prefixlen == sizeof raw->prefix) log_info ("%s: warning: prefix not terminated by a nul (block %llu)\n", filename, info->headerblock); for (n=prefixlen+1; n < sizeof raw->prefix; n++) if (raw->prefix[n]) { log_info ("%s: warning: garbage after prefix\n", filename); anyerror = 1; break; } } else prefixlen = 0; header = xtrycalloc (1, sizeof *header + prefixlen + 1 + namelen); if (!header) { log_error ("%s: error allocating header: %s\n", filename, gpg_strerror (gpg_error_from_syserror ())); return NULL; } if (prefixlen) { n = prefixlen; memcpy (header->name, raw->prefix, n); if (raw->prefix[n-1] != '/') header->name[n++] = '/'; } else n = 0; memcpy (header->name+n, raw->name, namelen); header->name[n+namelen] = 0; header->mode = parse_xoctal (raw->mode, sizeof raw->mode, filename); header->uid = parse_xoctal (raw->uid, sizeof raw->uid, filename); header->gid = parse_xoctal (raw->gid, sizeof raw->gid, filename); header->size = parse_xoctal (raw->size, sizeof raw->size, filename); header->mtime = parse_xoctal (raw->mtime, sizeof raw->mtime, filename); /* checksum = */ switch (raw->typeflag[0]) { case '0': header->typeflag = TF_REGULAR; break; case '1': header->typeflag = TF_HARDLINK; break; case '2': header->typeflag = TF_SYMLINK; break; case '3': header->typeflag = TF_CHARDEV; break; case '4': header->typeflag = TF_BLOCKDEV; break; case '5': header->typeflag = TF_DIRECTORY; break; case '6': header->typeflag = TF_FIFO; break; case '7': header->typeflag = TF_RESERVED; break; case 'g': header->typeflag = TF_GEXTHDR; break; case 'x': header->typeflag = TF_EXTHDR; break; default: header->typeflag = TF_UNKNOWN; break; } /* Compute the number of data records following this header. */ if (header->typeflag == TF_REGULAR || header->typeflag == TF_EXTHDR || header->typeflag == TF_UNKNOWN) header->nrecords = (header->size + RECORDSIZE-1)/RECORDSIZE; else header->nrecords = 0; if (anyerror) { log_info ("%s: header block %llu is corrupt" " (size=%llu type=%d nrec=%llu)\n", filename, info->headerblock, header->size, header->typeflag, header->nrecords); /* log_printhex (record, RECORDSIZE, " "); */ } return header; } /* Parse the extended header. This funcion may modify BUFFER. */ static gpg_error_t parse_extended_header (const char *fname, char *buffer, size_t buflen, strlist_t *r_exthdr) { unsigned int reclen; unsigned char *p, *record; strlist_t sl; while (buflen) { record = buffer; /* Remember begin of record. */ reclen = 0; for (p = buffer; buflen && digitp (p); buflen--, p++) { reclen *= 10; reclen += (*p - '0'); } if (!buflen || *p != ' ') { log_error ("%s: malformed record length in extended header\n", fname); return gpg_error (GPG_ERR_INV_RECORD); } p++; /* Skip space. */ buflen--; if (buflen + (p-record) < reclen) { log_error ("%s: extended header record larger" " than total extended header data\n", fname); return gpg_error (GPG_ERR_INV_RECORD); } if (reclen < (p-record)+2 || record[reclen-1] != '\n') { log_error ("%s: malformed extended header record\n", fname); return gpg_error (GPG_ERR_INV_RECORD); } record[reclen-1] = 0; /* For convenience change LF to a Nul. */ reclen -= (p-record); /* P points to the begin of the keyword and RECLEN is the * remaining length of the record excluding the LF. */ if (memchr (p, 0, reclen-1) && (!strncmp (p, "path=", 5) || !strncmp (p, "linkpath=", 9))) { log_error ("%s: extended header record has an embedded nul" " - ignoring\n", fname); } else if (!strncmp (p, "path=", 5)) { sl = add_to_strlist_try (r_exthdr, p+5); if (!sl) return gpg_error_from_syserror (); sl->flags = 1; /* Mark as path */ } else if (!strncmp (p, "linkpath=", 9)) { sl = add_to_strlist_try (r_exthdr, p+9); if (!sl) return gpg_error_from_syserror (); sl->flags = 2; /* Mark as linkpath */ } buffer = p + reclen; buflen -= reclen; } return 0; } /* Read the next block, assuming it is a tar header. Returns a header * object on success in R_HEADER, or an error. If the stream is * consumed (i.e. end-of-archive), R_HEADER is set to NULL. In case * of an error an error message is printed. If the header is an * extended header, a string list is allocated and stored at * R_EXTHEADER; the caller should provide a pointer to NULL. Such an * extended header is fully processed here and the returned R_HEADER * has then the next regular header. */ static gpg_error_t read_header (estream_t stream, tarinfo_t info, tar_header_t *r_header, strlist_t *r_extheader) { gpg_error_t err; char record[RECORDSIZE]; int i; tar_header_t hdr; char *buffer; size_t buflen, nrec; err = read_record (stream, record); if (err) return err; info->nblocks++; for (i=0; i < RECORDSIZE && !record[i]; i++) ; if (i == RECORDSIZE) { /* All zero header - check whether it is the first part of an end of archive mark. */ err = read_record (stream, record); if (err) return err; info->nblocks++; for (i=0; i < RECORDSIZE && !record[i]; i++) ; if (i != RECORDSIZE) log_info ("%s: warning: skipping empty header\n", es_fname_get (stream)); else { /* End of archive - FIXME: we might want to check for garbage. */ *r_header = NULL; return 0; } } *r_header = parse_header (record, es_fname_get (stream), info); if (!*r_header) return gpg_error_from_syserror (); hdr = *r_header; if (hdr->typeflag != TF_EXTHDR || !r_extheader) return 0; /* Read the extended header. */ if (!hdr->nrecords) { /* More than 64k for an extedned header is surely too large. */ log_info ("%s: warning: empty extended header\n", es_fname_get (stream)); return 0; } if (hdr->nrecords > 65536 / RECORDSIZE) { /* More than 64k for an extedned header is surely too large. */ log_error ("%s: extended header too large - skipping\n", es_fname_get (stream)); return 0; } buffer = xtrymalloc (hdr->nrecords * RECORDSIZE); if (!buffer) { err = gpg_error_from_syserror (); log_error ("%s: error allocating space for extended header: %s\n", es_fname_get (stream), gpg_strerror (err)); return err; } buflen = 0; for (nrec=0; nrec < hdr->nrecords;) { err = read_record (stream, buffer + buflen); if (err) { xfree (buffer); return err; } info->nblocks++; nrec++; if (nrec < hdr->nrecords || (hdr->size && !(hdr->size % RECORDSIZE))) buflen += RECORDSIZE; else buflen += (hdr->size % RECORDSIZE); } err = parse_extended_header (es_fname_get (stream), buffer, buflen, r_extheader); if (err) { free_strlist (*r_extheader); *r_extheader = NULL; } xfree (buffer); /* Now tha the extedned header has been read, we read the next * header without allowing an extended header. */ return read_header (stream, info, r_header, NULL); } /* Skip the data records according to HEADER. Prints an error message on error and return -1. */ static int skip_data (estream_t stream, tarinfo_t info, tar_header_t header) { char record[RECORDSIZE]; unsigned long long n; for (n=0; n < header->nrecords; n++) { if (read_record (stream, record)) return -1; info->nblocks++; } return 0; } static void print_header (tar_header_t header, strlist_t extheader, estream_t out) { unsigned long mask; char modestr[10+1]; int i; strlist_t sl; const char *name, *linkname; *modestr = '?'; switch (header->typeflag) { case TF_REGULAR: *modestr = '-'; break; case TF_HARDLINK: *modestr = 'h'; break; case TF_SYMLINK: *modestr = 'l'; break; case TF_CHARDEV: *modestr = 'c'; break; case TF_BLOCKDEV: *modestr = 'b'; break; case TF_DIRECTORY:*modestr = 'd'; break; case TF_FIFO: *modestr = 'f'; break; case TF_RESERVED: *modestr = '='; break; case TF_EXTHDR: break; case TF_GEXTHDR: break; case TF_UNKNOWN: break; case TF_NOTSUP: break; } for (mask = 0400, i = 0; i < 9; i++, mask >>= 1) modestr[1+i] = (header->mode & mask)? "rwxrwxrwx"[i]:'-'; if ((header->typeflag & 04000)) modestr[3] = modestr[3] == 'x'? 's':'S'; if ((header->typeflag & 02000)) modestr[6] = modestr[6] == 'x'? 's':'S'; if ((header->typeflag & 01000)) modestr[9] = modestr[9] == 'x'? 't':'T'; modestr[10] = 0; /* FIXME: We do not parse the linkname unless its part of an * extended header. */ name = header->name; linkname = header->typeflag == TF_SYMLINK? "?" : NULL; for (sl = extheader; sl; sl = sl->next) { if (sl->flags == 1) name = sl->d; else if (sl->flags == 2) linkname = sl->d; } es_fprintf (out, "%s %lu %lu/%lu %12llu %s %s%s%s\n", modestr, header->nlink, header->uid, header->gid, header->size, isotimestamp (header->mtime), name, linkname? " -> " : "", linkname? linkname : ""); } /* List the tarball FILENAME or, if FILENAME is NULL, the tarball read from stdin. */ gpg_error_t gpgtar_list (const char *filename, int decrypt) { gpg_error_t err; estream_t stream = NULL; tar_header_t header = NULL; strlist_t extheader = NULL; struct tarinfo_s tarinfo_buffer; tarinfo_t tarinfo = &tarinfo_buffer; - pid_t pid = (pid_t)(-1); + gnupg_process_t proc = NULL; memset (&tarinfo_buffer, 0, sizeof tarinfo_buffer); if (decrypt) { strlist_t arg; ccparray_t ccp; int except[2] = { -1, -1 }; const char **argv; ccparray_init (&ccp, 0); if (opt.batch) ccparray_put (&ccp, "--batch"); if (opt.require_compliance) ccparray_put (&ccp, "--require-compliance"); if (opt.status_fd != -1) { static char tmpbuf[40]; snprintf (tmpbuf, sizeof tmpbuf, "--status-fd=%d", opt.status_fd); ccparray_put (&ccp, tmpbuf); except[0] = opt.status_fd; } ccparray_put (&ccp, "--output"); ccparray_put (&ccp, "-"); ccparray_put (&ccp, "--decrypt"); for (arg = opt.gpg_arguments; arg; arg = arg->next) ccparray_put (&ccp, arg->d); if (filename) { ccparray_put (&ccp, "--"); ccparray_put (&ccp, filename); } ccparray_put (&ccp, NULL); argv = ccparray_get (&ccp, NULL); if (!argv) { err = gpg_error_from_syserror (); goto leave; } - err = gnupg_spawn_process (opt.gpg_program, argv, - except[0] == -1? NULL : except, - ((filename? 0 : GNUPG_SPAWN_KEEP_STDIN) - | GNUPG_SPAWN_KEEP_STDERR), - NULL, &stream, NULL, &pid); + err = gnupg_process_spawn (opt.gpg_program, argv, + ((filename ? 0 : GNUPG_PROCESS_STDIN_KEEP) + | GNUPG_PROCESS_STDOUT_PIPE), + gnupg_spawn_helper, except, &proc); xfree (argv); if (err) goto leave; + gnupg_process_get_streams (proc, 0, NULL, &stream, NULL); es_set_binary (stream); } else if (filename) /* No decryption requested. */ { if (!strcmp (filename, "-")) stream = es_stdin; else stream = es_fopen (filename, "rb,sysopen"); if (!stream) { err = gpg_error_from_syserror (); log_error ("error opening '%s': %s\n", filename, gpg_strerror (err)); goto leave; } if (stream == es_stdin) es_set_binary (es_stdin); } else { stream = es_stdin; es_set_binary (es_stdin); } for (;;) { err = read_header (stream, tarinfo, &header, &extheader); if (err || header == NULL) goto leave; print_header (header, extheader, es_stdout); if (skip_data (stream, tarinfo, header)) goto leave; free_strlist (extheader); extheader = NULL; xfree (header); header = NULL; } - if (pid != (pid_t)(-1)) + if (proc) { - int exitcode; - err = es_fclose (stream); stream = NULL; if (err) log_error ("error closing pipe: %s\n", gpg_strerror (err)); - else + + err = gnupg_process_wait (proc, 1); + if (!err) { - err = gnupg_wait_process (opt.gpg_program, pid, 1, &exitcode); - if (err) - log_error ("running %s failed (exitcode=%d): %s", - opt.gpg_program, exitcode, gpg_strerror (err)); - gnupg_release_process (pid); - pid = (pid_t)(-1); + int exitcode; + + gnupg_process_ctl (proc, GNUPG_PROCESS_GET_EXIT_ID, &exitcode); + log_error ("running %s failed (exitcode=%d): %s", + opt.gpg_program, exitcode, gpg_strerror (err)); } + gnupg_process_release (proc); + proc = NULL; } leave: free_strlist (extheader); xfree (header); if (stream != es_stdin) es_fclose (stream); return err; } gpg_error_t gpgtar_read_header (estream_t stream, tarinfo_t info, tar_header_t *r_header, strlist_t *r_extheader) { return read_header (stream, info, r_header, r_extheader); } void gpgtar_print_header (tar_header_t header, strlist_t extheader, estream_t out) { if (header && out) print_header (header, extheader, out); }