diff --git a/dirmngr/Makefile.am b/dirmngr/Makefile.am index bd70c8c3b..2a18a50d2 100644 --- a/dirmngr/Makefile.am +++ b/dirmngr/Makefile.am @@ -56,6 +56,7 @@ noinst_HEADERS = dirmngr.h crlcache.h crlfetch.h misc.h dirmngr_SOURCES = dirmngr.c dirmngr.h server.c crlcache.c crlfetch.c \ certcache.c certcache.h \ + loadswdb.c \ cdb.h cdblib.c misc.c dirmngr-err.h \ ocsp.c ocsp.h validate.c validate.h \ dns-stuff.c dns-stuff.h \ diff --git a/dirmngr/dirmngr.h b/dirmngr/dirmngr.h index 107059df4..42b3b2bba 100644 --- a/dirmngr/dirmngr.h +++ b/dirmngr/dirmngr.h @@ -206,5 +206,8 @@ gpg_error_t dirmngr_status_help (ctrl_t ctrl, const char *text); gpg_error_t dirmngr_tick (ctrl_t ctrl); +/*-- loadswdb.c --*/ +gpg_error_t dirmngr_load_swdb (ctrl_t ctrl, int force); + #endif /*DIRMNGR_H*/ diff --git a/dirmngr/loadswdb.c b/dirmngr/loadswdb.c new file mode 100644 index 000000000..57a7e0434 --- /dev/null +++ b/dirmngr/loadswdb.c @@ -0,0 +1,358 @@ +/* loadswdb.c - Load the swdb file from versions.gnupg.org + * 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 "dirmngr.h" +#include "../common/ccparray.h" +#include "../common/exectool.h" +#include "misc.h" +#include "ks-engine.h" + + +/* Get the time from the current swdb file and store it at R_TIME. If + * the file does not exist 0 is stored at R_TIME. The function + * returns 0 on sucess or an error code. */ +static gpg_error_t +time_of_saved_swdb (const char *fname, time_t *r_time) +{ + gpg_error_t err; + estream_t fp = NULL; + char *line = NULL; + size_t length_of_line = 0; + size_t maxlen; + ssize_t len; + char *fields[2]; + time_t t = (time_t)(-1); + + *r_time = 0; + + fp = es_fopen (fname, "r"); + err = fp? 0 : gpg_error_from_syserror (); + if (err) + { + if (gpg_err_code (err) == GPG_ERR_ENOENT) + err = 0; /* No file - assume time is the year of Unix. */ + goto leave; + } + + /* Note that the parser uses the first occurance 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); + 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 (!strcmp (fields[0], ".filedate")) + { + gnupg_isotime_t isot; + if (string2isotime (isot, fields[1]) + && (t = isotime2epoch (isot)) != (time_t)(-1)) + break; /* Got the time - stop reading. */ + } + } + if (len < 0 || es_ferror (fp)) + { + err = gpg_error_from_syserror (); + goto leave; + } + if (t == (time_t)(-1)) + { + err = gpg_error (GPG_ERR_INV_TIME); + goto leave; + } + + *r_time = t; + + leave: + if (err) + log_error (_("error reading '%s': %s\n"), fname, gpg_strerror (err)); + xfree (line); + es_fclose (fp); + return err; +} + + + +/* Read a file from URL and return it as an estream memory buffer at + * R_FP. */ +static gpg_error_t +fetch_file (ctrl_t ctrl, const char *url, estream_t *r_fp) +{ + gpg_error_t err; + estream_t fp = NULL; + estream_t httpfp = NULL; + size_t nread, nwritten; + char buffer[1024]; + + if ((err = ks_http_fetch (ctrl, url, &httpfp))) + goto leave; + + /* We now read the data from the web server into a memory buffer. + * To avoid excessive memory use in case of a ill behaving server we + * put a 64 k size limit on the buffer. As of today the actual size + * of the swdb.lst file is 3k. */ + fp = es_fopenmem (64*1024, "rw"); + if (!fp) + { + err = gpg_error_from_syserror (); + log_error ("error allocating memory buffer: %s\n", gpg_strerror (err)); + goto leave; + } + + for (;;) + { + if (es_read (httpfp, buffer, sizeof buffer, &nread)) + { + err = gpg_error_from_syserror (); + log_error ("error reading '%s': %s\n", + es_fname_get (httpfp), gpg_strerror (err)); + goto leave; + } + + if (!nread) + break; /* Ready. */ + if (es_write (fp, buffer, nread, &nwritten)) + { + err = gpg_error_from_syserror (); + log_error ("error writing '%s': %s\n", + es_fname_get (fp), gpg_strerror (err)); + goto leave; + } + else if (nread != nwritten) + { + err = gpg_error (GPG_ERR_EIO); + log_error ("error writing '%s': %s\n", + es_fname_get (fp), "short write"); + goto leave; + } + } + + es_rewind (fp); + *r_fp = fp; + fp = NULL; + + leave: + es_fclose (httpfp); + es_fclose (fp); + return err; +} + + +/* Communication object for verify_status_cb. */ +struct verify_status_parm_s +{ + time_t sigtime; + int anyvalid; +}; + +static void +verify_status_cb (void *opaque, const char *keyword, char *args) +{ + struct verify_status_parm_s *parm = opaque; + + /* We care only about the first valid signature. */ + if (!strcmp (keyword, "VALIDSIG") && !parm->anyvalid) + { + char *fields[3]; + + parm->anyvalid = 1; + if (split_fields (args, fields, DIM (fields)) >= 3) + parm->sigtime = parse_timestamp (fields[2], NULL); + } +} + + + +/* Load the swdb file into the current home directory. Do this onlky + * when needed unless FORCE is set which will always get a new + * copy. */ +gpg_error_t +dirmngr_load_swdb (ctrl_t ctrl, int force) +{ + gpg_error_t err; + char *fname = NULL; /* The swdb.lst file. */ + char *tmp_fname = NULL; /* The temporary swdb.lst file. */ + char *keyfile_fname = NULL; + estream_t swdb = NULL; + estream_t swdb_sig = NULL; + ccparray_t ccp; + const char **argv = NULL; + struct verify_status_parm_s verify_status_parm = { (time_t)(-1), 0 }; + estream_t outfp = NULL; + time_t now = gnupg_get_time (); + gnupg_isotime_t isotime; + + + fname = make_filename_try (gnupg_homedir (), "swdb.lst", NULL); + if (!fname) + { + err = gpg_error_from_syserror (); + goto leave; + } + + /* Check whether there is a need to get an update. */ + if (!force) + { + time_t filetime; + + err = time_of_saved_swdb (fname, &filetime); + if (err) + goto leave; + if (filetime >= now) + goto leave; /* Current or newer. */ + if (now - filetime < 3*86400) + goto leave; /* Not older than 3 days. */ + } + + /* Create the filename of the file with the keys. */ + keyfile_fname = make_filename_try (gnupg_datadir (), "distsigkey.gpg", NULL); + if (!keyfile_fname) + goto leave; + + /* Fetch the swdb from the web. */ + err = fetch_file (ctrl, "https://versions.gnupg.org/swdb.lst", &swdb); + if (err) + goto leave; + err = fetch_file (ctrl, "https://versions.gnupg.org/swdb.lst.sig", &swdb_sig); + if (err) + goto leave; + + /* Run gpgv. */ + ccparray_init (&ccp, 0); + ccparray_put (&ccp, "--enable-special-filenames"); + ccparray_put (&ccp, "--status-fd=2"); + ccparray_put (&ccp, "--keyring"); + ccparray_put (&ccp, keyfile_fname); + ccparray_put (&ccp, "--"); + ccparray_put (&ccp, "-&@INEXTRA@"); + ccparray_put (&ccp, "-"); + ccparray_put (&ccp, NULL); + argv = ccparray_get (&ccp, NULL); + if (!argv) + { + err = gpg_error_from_syserror (); + goto leave; + } + + err = gnupg_exec_tool_stream (gnupg_module_name (GNUPG_MODULE_NAME_GPGV), + argv, swdb, swdb_sig, NULL, + verify_status_cb, &verify_status_parm); + if (!err && verify_status_parm.sigtime == (time_t)(-1)) + err = gpg_error (verify_status_parm.anyvalid? GPG_ERR_BAD_SIGNATURE + /**/ : GPG_ERR_INV_TIME ); + if (err) + goto leave; + + /* Create a file name for a temporary file in the home directory. + * We will later rename that file to the real name. */ + { + char *tmpstr; + +#ifdef HAVE_W32_SYSTEM + tmpstr = es_bsprintf ("tmp-%u-swdb", (unsigned int)getpid ()); +#else + tmpstr = es_bsprintf (".#%u.swdb", (unsigned int)getpid ()); +#endif + if (!tmpstr) + { + err = gpg_error_from_syserror (); + goto leave; + } + tmp_fname = make_filename_try (gnupg_homedir (), tmpstr, NULL); + xfree (tmpstr); + if (!tmp_fname) + { + err = gpg_error_from_syserror (); + goto leave; + } + } + + outfp = es_fopen (tmp_fname, "w"); + if (!outfp) + { + err = gpg_error_from_syserror (); + log_error (_("error creating '%s': %s\n"), tmp_fname, gpg_strerror (err)); + goto leave; + } + + epoch2isotime (isotime, verify_status_parm.sigtime); + es_fprintf (outfp, ".filedate %s\n", isotime); + epoch2isotime (isotime, now); + es_fprintf (outfp, ".verified %s\n", isotime); + + if (es_fseek (swdb, 0, SEEK_SET)) + { + err = gpg_error_from_syserror (); + goto leave; + } + + err = copy_stream (swdb, outfp); + if (err) + { + /* Well, it might also be a reading error, but that is pretty + * unlikely for a memory stream. */ + log_error (_("error writing '%s': %s\n"), tmp_fname, gpg_strerror (err)); + goto leave; + } + + if (es_fclose (outfp)) + { + err = gpg_error_from_syserror (); + log_error (_("error writing '%s': %s\n"), tmp_fname, gpg_strerror (err)); + goto leave; + } + outfp = NULL; + + err = gnupg_rename_file (tmp_fname, fname, NULL); + if (err) + goto leave; + xfree (tmp_fname); + tmp_fname = NULL; + + + leave: + es_fclose (outfp); + if (tmp_fname) + gnupg_remove (tmp_fname); /* This is a temporary file. */ + xfree (argv); + es_fclose (swdb_sig); + es_fclose (swdb); + xfree (keyfile_fname); + xfree (tmp_fname); + xfree (fname); + return err; +} diff --git a/dirmngr/server.c b/dirmngr/server.c index 2122d548c..5c78d64c3 100644 --- a/dirmngr/server.c +++ b/dirmngr/server.c @@ -54,8 +54,6 @@ #include "mbox-util.h" #include "zb32.h" #include "server-help.h" -#include "ccparray.h" -#include "../common/exectool.h" /* To avoid DoS attacks we limit the size of a certificate to something reasonable. The DoS was actually only an issue back when @@ -2239,6 +2237,22 @@ cmd_ks_put (assuan_context_t ctx, char *line) } + +static const char hlp_loadswdb[] = + "LOADSWDB [--force]\n" + "\n" + "Load and verify the swdb.lst from the Net."; +static gpg_error_t +cmd_loadswdb (assuan_context_t ctx, char *line) +{ + ctrl_t ctrl = assuan_get_pointer (ctx); + gpg_error_t err; + + err = dirmngr_load_swdb (ctrl, has_option (line, "--force")); + + return leave_cmd (ctx, err); +} + static const char hlp_getinfo[] = @@ -2342,388 +2356,6 @@ cmd_reloaddirmngr (assuan_context_t ctx, char *line) } - -/* This function parses the first portion of the version number S and - * stores it in *NUMBER. On success, this function returns a pointer - * into S starting with the first character, which is not part of the - * initial number portion; on failure, NULL is returned. */ -static const char* -parse_version_number (const char *s, int *number) -{ - int val = 0; - - if (*s == '0' && digitp (&s[1])) - return NULL; /* Leading zeros are not allowed. */ - for (; digitp (s); s++) - { - val *= 10; - val += *s - '0'; - } - *number = val; - return val < 0 ? NULL : s; -} - - -/* This function breaks up the complete string-representation of the - * version number S, which is of the following struture: .[.]. The major, - * minor and micro number components will be stored in *MAJOR, *MINOR - * and *MICRO. If MICRO is not given 0 is used instead. - * - * On success, the last component, the patch level, will be returned; - * on failure, NULL will be returned. */ -static const char * -parse_version_string (const char *s, int *major, int *minor, int *micro) -{ - s = parse_version_number (s, major); - if (!s || *s != '.') - return NULL; - s++; - s = parse_version_number (s, minor); - if (!s) - return NULL; - if (*s == '.') - { - s++; - s = parse_version_number (s, micro); - if (!s) - return NULL; - } - else - micro = 0; - return s; /* Patchlevel. */ -} - - -/* Create temporary directory with mode 0700. Returns a dynamically - * allocated string with the filename of the directory. */ -static char * -my_mktmpdir (void) -{ - char *name, *p; - - p = getenv ("TMPDIR"); - if (!p || !*p) - p = "/tmp"; - if (p[strlen (p) - 1] == '/') - name = strconcat (p, "gpg-XXXXXX", NULL); - else - name = strconcat (p, "/", "gpg-XXXXXX", NULL); - if (!name || !gnupg_mkdtemp (name)) - { - int saveerr = errno; - log_error (_("can't create temporary directory '%s': %s\n"), - name, strerror (saveerr)); - gpg_err_set_errno (saveerr); - return NULL; - } - - return name; -} - - -/* Sets result to -1 if version a is less than b, 0 if the versions are equal - * and 1 otherwise. Patch levels are compared as strings. */ -static gpg_error_t -cmp_version (const char *a, const char *b, int *result) -{ - int a_major, b_major; - int a_minor, b_minor; - int a_micro, b_micro; - const char *a_patch, *b_patch; - - if (!a || !b || !result) - return gpg_error (GPG_ERR_EINVAL); - - a_patch = parse_version_string (a, &a_major, &a_minor, &a_micro); - b_patch = parse_version_string (b, &b_major, &b_minor, &b_micro); - - if (!a_patch || !b_patch) - return gpg_error (GPG_ERR_EINVAL); - - if (a_major == b_major) - { - if (a_minor == b_minor) - { - if (a_micro == b_micro) - *result = strcmp (a_patch, b_patch); - else - *result = a_micro - b_minor; - } - else - *result = a_minor - b_minor; - } - else - *result = a_major - b_major; - - return 0; -} - - -static gpg_error_t -fetch_into_tmpdir (ctrl_t ctrl, const char *url, estream_t *strm_out, - char **path) -{ - gpg_error_t err; - char *filename = NULL; - char *dirname = NULL; - estream_t file = NULL; - estream_t strm = NULL; - size_t len, nwritten; - char buf[1024]; - - if (!strm_out || !path || !url) - { - err = gpg_error (GPG_ERR_INV_ARG); - goto leave; - } - - dirname = my_mktmpdir (); - if (!dirname) - { - err = gpg_error_from_syserror (); - goto leave; - } - - filename = strconcat (dirname, DIRSEP_S, "file", NULL); - if (!filename) - { - err = gpg_error_from_syserror (); - goto leave; - } - - file = es_fopen (filename, "w+"); - if (!file) - { - err = gpg_error_from_syserror (); - goto leave; - } - - if ((err = ks_http_fetch (ctrl, url, &strm))) - goto leave; - - for (;;) - { - if (es_read (strm, buf, sizeof buf, &len)) - { - err = gpg_error_from_syserror (); - log_error ("error reading '%s': %s\n", - es_fname_get (strm), gpg_strerror (err)); - goto leave; - } - - if (!len) - break; - if (es_write (file, buf, len, &nwritten)) - { - err = gpg_error_from_syserror (); - log_error ("error writing '%s': %s\n", filename, gpg_strerror (err)); - goto leave; - } - else if (len != nwritten) - { - err = gpg_error (GPG_ERR_EIO); - log_error ("error writing '%s': %s\n", filename, "short write"); - goto leave; - } - } - - es_rewind (file); - *strm_out = file; - file = NULL; - - if (path) - { - *path = dirname; - dirname = NULL; - } - - leave: - es_fclose (file); - es_fclose (strm); - xfree (dirname); - xfree (filename); - return err; -} - - -struct verify_swdb_parm_s -{ - time_t sigtime; - int anyvalid; -}; - -static void -verify_swdb_status_cb (void *opaque, const char *keyword, char *args) -{ - struct verify_swdb_parm_s *parm = opaque; - - /* We care only about the first valid signature. */ - if (!strcmp (keyword, "VALIDSIG") && !parm->anyvalid) - { - char *fields[3]; - - parm->anyvalid = 1; - if (split_fields (args, fields, DIM (fields)) >= 3) - parm->sigtime = parse_timestamp (fields[2], NULL); - } -} - - -static const char hlp_versioncheck[] = - "VERSIONCHECK " - "\n" - "Checks the internet to find whenever a new program version is available." - "\n" - " program name i.e. \"gnupg\"\n" - " current version of the program i.e. \"2.0.2\""; -static gpg_error_t -cmd_versioncheck (assuan_context_t ctx, char *line) -{ - gpg_error_t err; - - char *name; - char *version; - size_t name_len; - char *cmd_fields[2]; - - ctrl_t ctrl; - estream_t swdb = NULL; - estream_t swdb_sig = NULL; - char* swdb_dir = NULL; - char* swdb_sig_dir = NULL; - char* buf = NULL; - size_t len = 0; - ccparray_t ccp; - const char **argv = NULL; - char keyring_name[128]; - char swdb_name[128]; - char swdb_sig_name[128]; - - struct verify_swdb_parm_s verify_swdb_parm = { (time_t)(-1), 0 }; - - - swdb_name[0] = 0; - swdb_sig_name[0] = 0; - ctrl = assuan_get_pointer (ctx); - - if (split_fields (line, cmd_fields, 2) != 2) - { - err = set_error (GPG_ERR_ASS_PARAMETER, - "No program name and/or version given"); - goto out; - } - - name = cmd_fields[0]; - name_len = strlen (name); - version = cmd_fields[1]; - - if ((err = fetch_into_tmpdir (ctrl, "https://versions.gnupg.org/swdb.lst", - &swdb, &swdb_dir))) - goto out; - - snprintf (swdb_name, sizeof swdb_name, "%s%s%s", swdb_dir, DIRSEP_S, "file"); - - if ((err = fetch_into_tmpdir (ctrl, "https://versions.gnupg.org/swdb.lst.sig", - &swdb_sig, &swdb_sig_dir))) - goto out; - - snprintf (keyring_name, sizeof keyring_name, "%s%s%s", gnupg_datadir (), - DIRSEP_S, "distsigkey.gpg"); - snprintf (swdb_sig_name, sizeof swdb_sig_name, "%s%s%s", swdb_sig_dir, - DIRSEP_S, "file"); - - ccparray_init (&ccp, 0); - ccparray_put (&ccp, "--status-fd=2"); - ccparray_put (&ccp, "--keyring"); - ccparray_put (&ccp, keyring_name); - ccparray_put (&ccp, "--"); - ccparray_put (&ccp, swdb_sig_name); - ccparray_put (&ccp, "-"); - ccparray_put (&ccp, NULL); - argv = ccparray_get (&ccp, NULL); - if (!argv) - { - err = gpg_error_from_syserror (); - goto out; - } - - if ((err = gnupg_exec_tool_stream (gnupg_module_name (GNUPG_MODULE_NAME_GPGV), - argv, swdb, NULL, NULL, - verify_swdb_status_cb, &verify_swdb_parm))) - goto out; - if (verify_swdb_parm.sigtime == (time_t)(-1)) - { - if (verify_swdb_parm.anyvalid) - err = gpg_error (GPG_ERR_BAD_SIGNATURE); - else - err = gpg_error (GPG_ERR_INV_TIME); - goto out; - } - - { - gnupg_isotime_t tbuf; - - epoch2isotime (tbuf, verify_swdb_parm.sigtime); - log_debug ("swdb created: %s\n", tbuf); - } - - es_fseek (swdb, 0, SEEK_SET); - - while (es_getline (&buf, &len, swdb) > 0) - { - if (len > name_len + 5 && - strncmp (buf, name, name_len) == 0 && - strncmp (buf + name_len, "_ver ", 5) == 0) - { - const char* this_ver_start = buf + name_len + 5; - char* this_ver_end = strchr (this_ver_start, '\n'); - int cmp; - - if (this_ver_end) - *this_ver_end = 0; - - err = assuan_write_status (ctx, "LINE", buf); - - err = cmp_version (this_ver_start, version, &cmp); - if (err > 0) - goto out; - - if (cmp < 0) - err = assuan_send_data (ctx, "ROLLBACK", strlen ("ROLLBACK")); - else if (cmp == 0) - err = assuan_send_data (ctx, "CURRENT", strlen ("CURRENT")); - else - err = assuan_send_data (ctx, "UPDATE", strlen ("UPDATE")); - - goto out; - } - } - - err = assuan_send_data (ctx, "NOT_FOUND", strlen ("NOT_FOUND")); - - out: - es_fclose (swdb); - es_fclose (swdb_sig); - xfree (buf); - - if (strlen (swdb_name) > 0) - remove (swdb_name); - if (swdb_dir) - rmdir (swdb_dir); - xfree (swdb_dir); - - if (strlen (swdb_sig_name) > 0) - remove (swdb_sig_name); - if (swdb_sig_dir) - rmdir (swdb_sig_dir); - xfree (swdb_sig_dir); - xfree (argv); - - return leave_cmd (ctx, err); -} - - /* Tell the assuan library about our commands. */ static int @@ -2751,9 +2383,9 @@ register_commands (assuan_context_t ctx) { "KS_FETCH", cmd_ks_fetch, hlp_ks_fetch }, { "KS_PUT", cmd_ks_put, hlp_ks_put }, { "GETINFO", cmd_getinfo, hlp_getinfo }, + { "LOADSWDB", cmd_loadswdb, hlp_loadswdb }, { "KILLDIRMNGR",cmd_killdirmngr,hlp_killdirmngr }, { "RELOADDIRMNGR",cmd_reloaddirmngr,hlp_reloaddirmngr }, - { "VERSIONCHECK",cmd_versioncheck,hlp_versioncheck }, { NULL, NULL } }; int i, j, rc;