/* 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_FILEDATE * and R_VERIFIED. If the file does not exist 0 is stored at there. * The function returns 0 on success or an error code. */ static gpg_error_t time_of_saved_swdb (const char *fname, time_t *r_filedate, time_t *r_verified) { 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]; gnupg_isotime_t isot; time_t filedate = (time_t)(-1); time_t verified = (time_t)(-1); *r_filedate = 0; *r_verified = 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 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); 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 == (time_t)(-1) && !strcmp (fields[0], ".filedate")) { if (string2isotime (isot, fields[1])) filedate = isotime2epoch (isot); } else if (verified == (time_t)(-1) && !strcmp (fields[0], ".verified")) { if (string2isotime (isot, fields[1])) verified = isotime2epoch (isot); } } if (len < 0 || es_ferror (fp)) { err = gpg_error_from_syserror (); goto leave; } if (filedate == (time_t)(-1) || verified == (time_t)(-1)) { err = gpg_error (GPG_ERR_INV_TIME); goto leave; } *r_filedate = filedate; *r_verified = verified; 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; if (DBG_EXTPROG) log_debug ("gpgv status: %s %s\n", keyword, args); /* 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 (); time_t filedate = 0; /* ".filedate" from our swdb. */ time_t verified = 0; /* ".verified" from our swdb. */ 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) { static int not_older_than; static time_t lastcheck; if (!not_older_than) { /* To balance access to the server we use a random time from * 5 to 7 days for update checks. */ not_older_than = 5 * 86400; not_older_than += (get_uint_nonce () % (2*86400)); } if (now - lastcheck < 3600) { /* We checked our swdb file in the last hour - don't check * again to avoid unnecessary disk access. */ err = 0; goto leave; } lastcheck = now; err = time_of_saved_swdb (fname, &filedate, &verified); if (gpg_err_code (err) == GPG_ERR_INV_TIME) err = 0; /* Force reading. */ if (err) goto leave; if (filedate >= now) goto leave; /* Current or newer. */ if (now - filedate < not_older_than) goto leave; /* Our copy is pretty new (not older than 7 days). */ if (verified > now && now - verified < 3*3600) goto leave; /* We downloaded and verified in the last 3 hours. */ } /* Create the filename of the file with the keys. */ keyfile_fname = make_filename_try (gnupg_datadir (), "distsigkey.gpg", NULL); if (!keyfile_fname) { err = gpg_error_from_syserror (); 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; } if (DBG_EXTPROG) log_debug ("starting gpgv\n"); 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 (DBG_EXTPROG) log_debug ("gpgv finished: err=%d\n", err); if (err) goto leave; /* If our swdb is not older than the downloaded one. We don't * bother to update. */ if (!force && filedate >= verify_status_parm.sigtime) 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; }