g10: Be careful to not be in a transaction during long operations
[gnupg.git] / g10 / tofu.c
index ed8bbbe..de685a6 100644 (file)
@@ -39,7 +39,7 @@
 #include "ttyio.h"
 #include "trustdb.h"
 #include "mkdir_p.h"
-#include "sqlite.h"
+#include "gpgsql.h"
 #include "status.h"
 
 #include "tofu.h"
 
 #define CONTROL_L ('L' - 'A' + 1)
 
-/* Number of signed messages required to not show extra warnings.  */
-#define NO_WARNING_THRESHOLD 10
+/* Number of signed messages required to indicate that enough history
+ * is available for basic trust.  */
+#define BASIC_TRUST_THRESHOLD  10
+/* Number of signed messages required to indicate that a lot of
+ * history is available.  */
+#define FULL_TRUST_THRESHOLD  100
 
 
-#define DEBUG_TOFU_CACHE 0
-#if DEBUG_TOFU_CACHE
-static int prepares_saved;
-static int queries;
-#endif
-
-/* The TOFU data can be saved in two different formats: either in a
-   single combined database (opt.tofu_db_format == TOFU_DB_FLAT) or in
-   a split file format (opt.tofu_db_format == TOFU_DB_SPLIT).  In the
-   split format, there is one database per normalized email address
-   (DB_EMAIL) and one per key (DB_KEY).  */
-enum db_type
-  {
-    DB_COMBINED,
-    DB_EMAIL,
-    DB_KEY
-  };
-
-/* A list of open DBs.
+/* An struct with data pertaining to the tofu DB.
 
-   In the flat format, this consists of a single element with the type
-   DB_COMBINED and whose name is the empty string.
-
-   In the split format, the first element is a dummy element (DB is
-   NULL) whose type is DB_COMBINED and whose name is the empty string.
-   Any following elements describe either DB_EMAIL or DB_KEY DBs.  In
-   theis case, NAME is either the normalized email address or the
-   fingerprint.
-
-   To initialize this data structure, call opendbs().  When you are
-   done, clean it up using closedbs().  To get a handle to a database,
+   To initialize this data structure, call opendbs().  Cleanup is done
+   when the CTRL object is released.  To get a handle to a database,
    use the getdb() function.  This will either return an existing
    handle or open a new DB connection, as appropriate.  */
-struct db
+struct tofu_dbs_s
 {
-  struct db *next;
-  struct db **prevp;
-
-  enum db_type type;
-
   sqlite3 *db;
 
   struct
@@ -98,9 +70,6 @@ struct db
     sqlite3_stmt *savepoint_batch;
     sqlite3_stmt *savepoint_batch_commit;
 
-    sqlite3_stmt *savepoint_inner;
-    sqlite3_stmt *savepoint_inner_commit;
-
     sqlite3_stmt *record_binding_get_old_policy;
     sqlite3_stmt *record_binding_update;
     sqlite3_stmt *record_binding_update2;
@@ -112,34 +81,11 @@ struct db
     sqlite3_stmt *register_insert;
   } s;
 
-#if DEBUG_TOFU_CACHE
-  int hits;
-#endif
-
-  int batch_update;
-
-  /* If TYPE is DB_COMBINED, this is "".  Otherwise, it is either the
-     fingerprint (type == DB_KEY) or the normalized email address
-     (type == DB_EMAIL).  */
-  char name[1];
+  int in_batch_transaction;
+  int in_transaction;
+  time_t batch_update_started;
 };
 
-static struct db *db_cache;
-static int db_cache_count;
-#define DB_CACHE_ENTRIES 16
-
-static void tofu_cache_dump (struct db *db) GPGRT_ATTR_USED;
-
-static void
-tofu_cache_dump (struct db *db)
-{
-  log_info ("Connection %p:\n", db);
-  for (; db; db = db->next)
-    log_info ("  %s: %sbatch mode\n", db->name, db->batch_update ? "" : "NOT ");
-  log_info ("Cache:\n");
-  for (db = db_cache; db; db = db->next)
-    log_info ("  %s: %sbatch mode\n", db->name, db->batch_update ? "" : "NOT ");
-}
 
 #define STRINGIFY(s) STRINGIFY2(s)
 #define STRINGIFY2(s) #s
@@ -163,8 +109,12 @@ tofu_cache_dump (struct db *db)
 #  define TIME_AGO_UNIT_LARGE (30 * 24 * 60 * 60)
 #endif
 
-\f
+/* Local prototypes.  */
+static gpg_error_t end_transaction (ctrl_t ctrl, int only_batch);
+static char *email_from_user_id (const char *user_id);
 
+
+\f
 const char *
 tofu_policy_str (enum tofu_policy policy)
 {
@@ -209,83 +159,82 @@ tofu_policy_to_trust_level (enum tofu_policy policy)
       return 0;
     }
 }
-\f
-static int batch_update;
-static time_t batch_update_started;
 
-static gpg_error_t end_transaction (struct db *db, int only_batch);
 
-/* Start a transaction on DB.  */
+\f
+/* Start a transaction on DB.  If ONLY_BATCH is set, then this will
+   start a batch transaction if we haven't started a batch transaction
+   and one has been requested.  */
 static gpg_error_t
-begin_transaction (struct db *db, int only_batch)
+begin_transaction (ctrl_t ctrl, int only_batch)
 {
+  tofu_dbs_t dbs = ctrl->tofu.dbs;
   int rc;
   char *err = NULL;
 
-  if (batch_update && batch_update_started != gnupg_get_time ())
-    /* We've been in batch update mode for a while (on average, more
-       than 500 ms).  To prevent starving other gpg processes, we drop
-       and retake the batch lock.
+  log_assert (dbs);
 
-       Note: if we wanted higher resolution, we could use
-       npth_clock_gettime.  */
+  /* If we've been in batch update mode for a while (on average, more
+   * than 500 ms), to prevent starving other gpg processes, we drop
+   * and retake the batch lock.
+   *
+   * Note: if we wanted higher resolution, we could use
+   * npth_clock_gettime.  */
+  if (/* No real transactions.  */
+      dbs->in_transaction == 0
+      /* There is an open batch transaction.  */
+      && dbs->in_batch_transaction
+      /* And some time has gone by since it was started.  */
+      && dbs->batch_update_started != gnupg_get_time ())
     {
-      struct db *t;
+      /* If we are in a batch update, then batch updates better have
+         been enabled.  */
+      log_assert (ctrl->tofu.batch_updated_wanted);
 
-      for (t = db_cache; t; t = t->next)
-        if (t->batch_update)
-          end_transaction (t, 1);
-      for (t = db; t; t = t->next)
-        if (t->batch_update)
-          end_transaction (t, 1);
-
-      batch_update_started = gnupg_get_time ();
+      end_transaction (ctrl, 2);
 
       /* Yield to allow another process a chance to run.  */
       gpgrt_yield ();
     }
 
-  /* XXX: In split mode, this can end in deadlock.
-
-     Consider: we have two gpg processes running simultaneously and
-     they each want to lock DB A and B, but in different orders.  This
-     will be automatically resolved by causing one of them to return
-     EBUSY and aborting.
-
-     A more intelligent approach would be to commit and retake the
-     batch transaction.  This requires a list of all DBs that are
-     currently in batch mode.  */
-
-  if (batch_update && ! db->batch_update)
+  if (/* Batch mode is enabled.  */
+      ctrl->tofu.batch_updated_wanted
+      /* But we don't have an open batch transaction.  */
+      && !dbs->in_batch_transaction)
     {
-      rc = sqlite3_stepx (db->db, &db->s.savepoint_batch,
+      /* We are in batch mode, but we don't have an open batch
+       * transaction.  Since the batch save point must be the outer
+       * save point, it must be taken before the inner save point.  */
+      log_assert (dbs->in_transaction == 0);
+
+      rc = gpgsql_stepx (dbs->db, &dbs->s.savepoint_batch,
                           NULL, NULL, &err,
-                          "savepoint batch;", SQLITE_ARG_END);
+                          "savepoint batch;", GPGSQL_ARG_END);
       if (rc)
         {
           log_error (_("error beginning transaction on TOFU database: %s\n"),
                      err);
-          print_further_info ("batch, database '%s'",
-                              *db->name ? db->name : "[combined]");
           sqlite3_free (err);
           return gpg_error (GPG_ERR_GENERAL);
         }
 
-      db->batch_update = 1;
+      dbs->in_batch_transaction = 1;
+      dbs->batch_update_started = gnupg_get_time ();
     }
 
   if (only_batch)
     return 0;
 
-  rc = sqlite3_stepx (db->db, &db->s.savepoint_inner,
-                      NULL, NULL, &err,
-                      "savepoint inner;", SQLITE_ARG_END);
+  log_assert(dbs->in_transaction >= 0);
+  dbs->in_transaction ++;
+
+  rc = gpgsql_exec_printf (dbs->db, NULL, NULL, &err,
+                           "savepoint inner%d;",
+                           dbs->in_transaction);
   if (rc)
     {
       log_error (_("error beginning transaction on TOFU database: %s\n"),
                  err);
-      print_further_info ("inner, database '%s'",
-                          *db->name ? db->name : "[combined]");
       sqlite3_free (err);
       return gpg_error (GPG_ERR_GENERAL);
     }
@@ -293,55 +242,65 @@ begin_transaction (struct db *db, int only_batch)
   return 0;
 }
 
+
 /* Commit a transaction.  If ONLY_BATCH is 1, then this only ends the
  batch transaction if we have left batch mode.  If ONLY_BATCH is 2,
  this ends any open batch transaction even if we are still in batch
  mode.  */
* batch transaction if we have left batch mode.  If ONLY_BATCH is 2,
* this ends any open batch transaction even if we are still in batch
* mode.  */
 static gpg_error_t
-end_transaction (struct db *db, int only_batch)
+end_transaction (ctrl_t ctrl, int only_batch)
 {
+  tofu_dbs_t dbs = ctrl->tofu.dbs;
   int rc;
   char *err = NULL;
 
-  if (!db)
-    return 0;  /* Shortcut to allow for easier cleanup code.  */
-
-  if ((! batch_update || only_batch == 2) && db->batch_update)
-    /* The batch transaction is still in open, but we left batch
-       mode.  */
+  if (only_batch)
     {
-      db->batch_update = 0;
+      if (!dbs)
+        return 0;  /* Shortcut to allow for easier cleanup code.  */
 
-      rc = sqlite3_stepx (db->db, &db->s.savepoint_batch_commit,
-                          NULL, NULL, &err,
-                          "release batch;", SQLITE_ARG_END);
-      if (rc)
+      /* If we are releasing the batch transaction, then we better not
+         be in a normal transaction.  */
+      log_assert (dbs->in_transaction == 0);
+
+      if (/* Batch mode disabled?  */
+          (!ctrl->tofu.batch_updated_wanted || only_batch == 2)
+          /* But, we still have an open batch transaction?  */
+          && dbs->in_batch_transaction)
         {
-          log_error (_("error committing transaction on TOFU database: %s\n"),
-                     err);
-          print_further_info ("batch, database '%s'",
-                              *db->name ? db->name : "[combined]");
-          sqlite3_free (err);
-          return gpg_error (GPG_ERR_GENERAL);
+          /* The batch transaction is still in open, but we've left
+           * batch mode.  */
+          dbs->in_batch_transaction = 0;
+
+          rc = gpgsql_stepx (dbs->db, &dbs->s.savepoint_batch_commit,
+                             NULL, NULL, &err,
+                             "release batch;", GPGSQL_ARG_END);
+          if (rc)
+            {
+              log_error (_("error committing transaction on TOFU database: %s\n"),
+                         err);
+              sqlite3_free (err);
+              return gpg_error (GPG_ERR_GENERAL);
+            }
+
+          return 0;
         }
 
-      /* Releasing an outer transaction releases an open inner
-         transactions.  We're done.  */
       return 0;
     }
 
-  if (only_batch)
-    return 0;
+  log_assert (dbs);
+  log_assert (dbs->in_transaction > 0);
+
+  rc = gpgsql_exec_printf (dbs->db, NULL, NULL, &err,
+                           "release inner%d;", dbs->in_transaction);
+
+  dbs->in_transaction --;
 
-  rc = sqlite3_stepx (db->db, &db->s.savepoint_inner_commit,
-                      NULL, NULL, &err,
-                      "release inner;", SQLITE_ARG_END);
   if (rc)
     {
       log_error (_("error committing transaction on TOFU database: %s\n"),
                  err);
-      print_further_info ("inner, database '%s'",
-                          *db->name ? db->name : "[combined]");
       sqlite3_free (err);
       return gpg_error (GPG_ERR_GENERAL);
     }
@@ -349,29 +308,29 @@ end_transaction (struct db *db, int only_batch)
   return 0;
 }
 
+
 static gpg_error_t
-rollback_transaction (struct db *db)
+rollback_transaction (ctrl_t ctrl)
 {
+  tofu_dbs_t dbs = ctrl->tofu.dbs;
   int rc;
   char *err = NULL;
 
-  if (!db)
-    return 0;  /* Shortcut to allow for easier cleanup code.  */
+  log_assert (dbs);
+  log_assert (dbs->in_transaction > 0);
 
-  if (db->batch_update)
-    /* Just undo the most recent update; don't revert any progress
-       made by the batch transaction.  */
-    rc = sqlite3_exec (db->db, "rollback to inner;", NULL, NULL, &err);
-  else
-    /* Rollback the whole she-bang.  */
-    rc = sqlite3_exec (db->db, "rollback;", NULL, NULL, &err);
+  /* Be careful to not any progress made by closed transactions in
+     batch mode.  */
+  rc = gpgsql_exec_printf (dbs->db, NULL, NULL, &err,
+                           "rollback to inner%d;",
+                           dbs->in_transaction);
+
+  dbs->in_transaction --;
 
   if (rc)
     {
       log_error (_("error rolling back transaction on TOFU database: %s\n"),
                  err);
-      print_further_info ("inner, database '%s'",
-                          *db->name ? db->name : "[combined]");
       sqlite3_free (err);
       return gpg_error (GPG_ERR_GENERAL);
     }
@@ -380,29 +339,38 @@ rollback_transaction (struct db *db)
 }
 
 void
-tofu_begin_batch_update (void)
+tofu_begin_batch_update (ctrl_t ctrl)
 {
-  if (! batch_update)
-    batch_update_started = gnupg_get_time ();
-
-  batch_update ++;
+  ctrl->tofu.batch_updated_wanted ++;
 }
 
 void
-tofu_end_batch_update (void)
+tofu_end_batch_update (ctrl_t ctrl)
 {
-  log_assert (batch_update > 0);
-  batch_update --;
-
-  if (batch_update == 0)
-    {
-      struct db *db;
+  log_assert (ctrl->tofu.batch_updated_wanted > 0);
+  ctrl->tofu.batch_updated_wanted --;
+  end_transaction (ctrl, 1);
+}
 
-      for (db = db_cache; db; db = db->next)
-        end_transaction (db, 1);
-    }
+/* Suspend any extant batch transaction (it is safe to call this even
+   no batch transaction has been started).  Note: you cannot suspend a
+   batch transaction if you are in a normal transaction.  The batch
+   transaction can be resumed explicitly by calling
+   tofu_resume_batch_transaction or implicitly by starting a normal
+   transaction.  */
+static void
+tofu_suspend_batch_transaction (ctrl_t ctrl)
+{
+  end_transaction (ctrl, 2);
 }
 
+/* Resume a batch transaction if there is no extant batch transaction
+   and one has been requested using tofu_begin_batch_transaction.  */
+static void
+tofu_resume_batch_transaction (ctrl_t ctrl)
+{
+  begin_transaction (ctrl, 1);
+}
 
 
 \f
@@ -519,7 +487,7 @@ version_check_cb (void *cookie, int argc, char **argv, char **azColName)
 
    Return 0 if the database is okay and 1 otherwise.  */
 static int
-initdb (sqlite3 *db, enum db_type type)
+initdb (sqlite3 *db)
 {
   char *err = NULL;
   int rc;
@@ -635,8 +603,7 @@ initdb (sqlite3 *db, enum db_type type)
    *    the old binding's policy to ask if it was auto.  So that we
    *     know why this occurred, we also set conflict to 0xbaddecaf.
    */
-  if (type == DB_EMAIL || type == DB_COMBINED)
-    rc = sqlite3_exec_printf
+  rc = gpgsql_exec_printf
       (db, NULL, NULL, &err,
        "create table bindings\n"
        " (oid INTEGER PRIMARY KEY AUTOINCREMENT,\n"
@@ -649,20 +616,6 @@ initdb (sqlite3 *db, enum db_type type)
        "create index bindings_email on bindings (email);\n",
        TOFU_POLICY_AUTO, TOFU_POLICY_GOOD, TOFU_POLICY_UNKNOWN,
        TOFU_POLICY_BAD, TOFU_POLICY_ASK);
-  else
-    /* In the split DB case, the fingerprint DB only contains a subset
-       of the fields.  This reduces the amount of duplicated data.
-
-       Note: since the data is split on the email address, there is no
-       need to index the email column.  */
-    rc = sqlite3_exec_printf
-      (db, NULL, NULL, &err,
-       "create table bindings\n"
-       " (oid INTEGER PRIMARY KEY AUTOINCREMENT,\n"
-       "  fingerprint TEXT, email TEXT, user_id,\n"
-       "  unique (fingerprint, email));\n"
-       "create index bindings_fingerprint\n"
-       " on bindings (fingerprint);\n");
   if (rc)
     {
       log_error (_("error initializing TOFU database: %s\n"), err);
@@ -671,35 +624,32 @@ initdb (sqlite3 *db, enum db_type type)
       goto out;
     }
 
-  if (type != DB_KEY)
-    {
-      /* The signatures that we have observed.
-
-        BINDING refers to a record in the bindings table, which
-         describes the binding (i.e., this is a foreign key that
-         references bindings.oid).
-
-        SIG_DIGEST is the digest stored in the signature.
-
-        SIG_TIME is the timestamp stored in the signature.
-
-        ORIGIN is a free-form string that describes who fed this
-         signature to GnuPG (e.g., email:claws).
-
-        TIME is the time this signature was registered.  */
-      rc = sqlite3_exec (db,
+  /* The signatures that we have observed.
+   *
+   * BINDING refers to a record in the bindings table, which
+   * describes the binding (i.e., this is a foreign key that
+   * references bindings.oid).
+   *
+   * SIG_DIGEST is the digest stored in the signature.
+   *
+   * SIG_TIME is the timestamp stored in the signature.
+   *
+   * ORIGIN is a free-form string that describes who fed this
+   * signature to GnuPG (e.g., email:claws).
+   *
+   * TIME is the time this signature was registered.  */
+  rc = sqlite3_exec (db,
                         "create table signatures "
                         " (binding INTEGER NOT NULL, sig_digest TEXT,"
                         "  origin TEXT, sig_time INTEGER, time INTEGER,"
                         "  primary key (binding, sig_digest, origin));",
                         NULL, NULL, &err);
-      if (rc)
-       {
-          log_error (_("error initializing TOFU database: %s\n"), err);
-          print_further_info ("create signatures");
-         sqlite3_free (err);
-         goto out;
-       }
+  if (rc)
+    {
+      log_error (_("error initializing TOFU database: %s\n"), err);
+      print_further_info ("create signatures");
+      sqlite3_free (err);
+      goto out;
     }
 
  out:
@@ -728,383 +678,80 @@ initdb (sqlite3 *db, enum db_type type)
     }
 }
 
-/* Open and initialize a low-level TOFU database.  Returns NULL on
-   failure.  This function should not normally be directly called to
-   get a database handle.  Instead, use getdb().  */
-static sqlite3 *
-opendb (char *filename, enum db_type type)
+
+/* Create a new DB handle.  Returns NULL on error.  */
+/* FIXME: Change to return an error code for better reporting by the
+   caller.  */
+static tofu_dbs_t
+opendbs (ctrl_t ctrl)
 {
+  char *filename;
   sqlite3 *db;
-  int filename_free = 0;
   int rc;
 
-  if (opt.tofu_db_format == TOFU_DB_FLAT)
+  if (!ctrl->tofu.dbs)
     {
-      log_assert (! filename);
-      log_assert (type == DB_COMBINED);
-
-      filename = make_filename (opt.homedir, "tofu.db", NULL);
-      filename_free = 1;
-    }
-  else
-    log_assert (type == DB_EMAIL || type == DB_KEY);
-
-  log_assert (filename);
-
-  rc = sqlite3_open (filename, &db);
-  if (rc)
-    {
-      log_error (_("error opening TOFU database '%s': %s\n"),
-                 filename, sqlite3_errmsg (db));
-      /* Even if an error occurs, DB is guaranteed to be valid.  */
-      sqlite3_close (db);
-      db = NULL;
-    }
-
-  /* If a DB is locked wait up to 5 seconds for the lock to be cleared
-     before failing.  */
-  if (db)
-    sqlite3_busy_timeout (db, 5 * 1000);
-
-  if (filename_free)
-    xfree (filename);
-
-  if (db && initdb (db, type))
-    {
-      sqlite3_close (db);
-      db = NULL;
-    }
+      filename = make_filename (gnupg_homedir (), "tofu.db", NULL);
 
-  return db;
-}
-
-struct dbs
-{
-  struct db *db;
-};
-
-static void
-unlink_db (struct db *db)
-{
-  *db->prevp = db->next;
-  if (db->next)
-    db->next->prevp = db->prevp;
-}
-
-static void
-link_db (struct db **head, struct db *db)
-{
-  db->next = *head;
-  if (db->next)
-    db->next->prevp = &db->next;
-  db->prevp = head;
-  *head = db;
-}
-
-/* Return a database handle.  <type, name> describes the required
-   database.  If there is a cached handle in DBS, that handle is
-   returned.  Otherwise, the database is opened and cached in DBS.
-
-   NAME is the name of the DB and may not be NULL.
-
-   TYPE must be either DB_MAIL or DB_KEY.  In the combined format, the
-   combined DB is always returned.  */
-static struct db *
-getdb (struct dbs *dbs, const char *name, enum db_type type)
-{
-  struct db *t = NULL;
-  char *name_sanitized = NULL;
-  int count;
-  char *filename = NULL;
-  int need_link = 1;
-  sqlite3 *sqlitedb = NULL;
-  gpg_error_t rc;
-
-  log_assert (dbs);
-  log_assert (name);
-  log_assert (type == DB_EMAIL || type == DB_KEY);
-
-  if (opt.tofu_db_format == TOFU_DB_FLAT)
-    /* When using the flat format, we only have a single DB, the
-       combined DB.  */
-    {
-      if (dbs->db)
+      rc = sqlite3_open (filename, &db);
+      if (rc)
         {
-          log_assert (dbs->db->type == DB_COMBINED);
-          log_assert (! dbs->db->next);
-          return dbs->db;
+          log_error (_("error opening TOFU database '%s': %s\n"),
+                     filename, sqlite3_errmsg (db));
+          /* Even if an error occurs, DB is guaranteed to be valid.  */
+          sqlite3_close (db);
+          db = NULL;
         }
+      xfree (filename);
 
-      type = DB_COMBINED;
-    }
+      /* If a DB is locked wait up to 5 seconds for the lock to be cleared
+         before failing.  */
+      if (db)
+        sqlite3_busy_timeout (db, 5 * 1000);
 
-  if (type != DB_COMBINED)
-    /* Only allow alpha-numeric characters in the name.  */
-    {
-      int i;
+      if (db && initdb (db))
+        {
+          sqlite3_close (db);
+          db = NULL;
+        }
 
-      name_sanitized = xstrdup (name);
-      for (i = 0; name[i]; i ++)
+      if (db)
         {
-          char c = name_sanitized[i];
-          if (! (('a' <= c && c <= 'z')
-                 || ('A' <= c && c <= 'Z')
-                 || ('0' <= c && c <= '9')))
-            name_sanitized[i] = '_';
+          ctrl->tofu.dbs = xmalloc_clear (sizeof *ctrl->tofu.dbs);
+          ctrl->tofu.dbs->db = db;
         }
     }
-
-  /* See if the DB is cached.  */
-  for (t = dbs->db; t; t = t->next)
-    if (t->type == type
-        && (type == DB_COMBINED || strcmp (t->name, name_sanitized) == 0))
-      {
-        need_link = 0;
-        goto out;
-      }
-
-  for (t = db_cache, count = 0; t; t = t->next, count ++)
-    if (type == t->type
-        && (type == DB_COMBINED || strcmp (t->name, name_sanitized) == 0))
-      {
-        unlink_db (t);
-        db_cache_count --;
-        goto out;
-      }
-
-  log_assert (db_cache_count == count);
-
-  if (type == DB_COMBINED)
-    filename = NULL;
   else
-    {
-      /* Open the DB.  The filename has the form:
-
-         tofu.d/TYPE/PREFIX/NAME.db
-
-         We use a short prefix to try to avoid having many files in a
-         single directory.  */
-      {
-        char *type_str = type == DB_EMAIL ? "email" : "key";
-        char prefix[3] = { name_sanitized[0], name_sanitized[1], 0 };
-        char *name_db;
-
-        /* Make the directory.  */
-        rc = gnupg_mkdir_p (opt.homedir, "tofu.d", type_str, prefix, NULL);
-        if (rc)
-          {
-            name_db = xstrconcat (opt.homedir, "tofu.d",
-                                  type_str, prefix, NULL);
-            log_error (_("can't create directory '%s': %s\n"),
-                       name_db, gpg_strerror (rc));
-            xfree (name_db);
-            goto out;
-          }
-
-        name_db = xstrconcat (name_sanitized, ".db", NULL);
-        filename = make_filename
-          (opt.homedir, "tofu.d", type_str, prefix, name_db, NULL);
-        xfree (name_db);
-      }
-    }
-
-  sqlitedb = opendb (filename, type);
-  if (! sqlitedb)
-    goto out;
-
-  t = xmalloc_clear (sizeof (struct db)
-                     + (name_sanitized ? strlen (name_sanitized) : 0));
-  t->type = type;
-  t->db = sqlitedb;
-  if (name_sanitized)
-    strcpy (t->name, name_sanitized);
-
- out:
-  if (t && need_link)
-    link_db (&dbs->db, t);
-
-#if DEBUG_TOFU_CACHE
-  if (t)
-    t->hits ++;
-#endif
+    log_assert (ctrl->tofu.dbs->db);
 
-  xfree (filename);
-  xfree (name_sanitized);
-  return t;
+  return ctrl->tofu.dbs;
 }
 
-static void
-closedb (struct db *db)
+
+/* Release all of the resources associated with the DB handle.  */
+void
+tofu_closedbs (ctrl_t ctrl)
 {
+  tofu_dbs_t dbs;
   sqlite3_stmt **statements;
 
-  if (opt.tofu_db_format == TOFU_DB_FLAT)
-    /* If we are using the flat format, then there is only ever the
-       combined DB.  */
-    log_assert (! db->next);
+  dbs = ctrl->tofu.dbs;
+  if (!dbs)
+    return;  /* Not initialized.  */
 
-  if (db->type == DB_COMBINED)
-    {
-      log_assert (opt.tofu_db_format == TOFU_DB_FLAT);
-      log_assert (! db->name[0]);
-    }
-  else
-    {
-      log_assert (opt.tofu_db_format == TOFU_DB_SPLIT);
-      log_assert (db->type != DB_COMBINED);
-      log_assert (db->name[0]);
-    }
+  log_assert (dbs->in_transaction == 0);
 
-  if (db->batch_update)
-    end_transaction (db, 2);
+  end_transaction (ctrl, 2);
 
-  for (statements = (void *) &db->s;
-       (void *) statements < (void *) &(&db->s)[1];
+  /* Arghh, that is a surprising use of the struct.  */
+  for (statements = (void *) &dbs->s;
+       (void *) statements < (void *) &(&dbs->s)[1];
        statements ++)
     sqlite3_finalize (*statements);
 
-  sqlite3_close (db->db);
-
-#if DEBUG_TOFU_CACHE
-  log_debug ("Freeing db.  Used %d times.\n", db->hits);
-#endif
-
-  xfree (db);
-}
-
-
-/* Create a new DB meta-handle.  Returns NULL on error.  */
-/* FIXME: Change to return an error code for better reporting by the
-   caller.  */
-static struct dbs *
-opendbs (void)
-{
-  if (opt.tofu_db_format == TOFU_DB_AUTO)
-    {
-      char *filename = make_filename (opt.homedir, "tofu.db", NULL);
-      struct stat s;
-      int have_tofu_db = 0;
-      int have_tofu_d = 0;
-
-      if (stat (filename, &s) == 0)
-       {
-         have_tofu_db = 1;
-         if (DBG_TRUST)
-           log_debug ("%s exists.\n", filename);
-       }
-      else
-       {
-         if (DBG_TRUST)
-           log_debug ("%s does not exist.\n", filename);
-       }
-
-      /* We now have tofu.d.  */
-      filename[strlen (filename) - 1] = '\0';
-      if (stat (filename, &s) == 0)
-       {
-         have_tofu_d = 1;
-         if (DBG_TRUST)
-           log_debug ("%s exists.\n", filename);
-       }
-      else
-       {
-         if (DBG_TRUST)
-           log_debug ("%s does not exist.\n", filename);
-       }
-
-      xfree (filename);
-
-      if (have_tofu_db && have_tofu_d)
-       {
-         log_info (_("Warning: Home directory contains both tofu.db"
-                      " and tofu.d.\n"));
-          log_info (_("Using split format for TOFU database\n"));
-         opt.tofu_db_format = TOFU_DB_SPLIT;
-       }
-      else if (have_tofu_db)
-       {
-         opt.tofu_db_format = TOFU_DB_FLAT;
-         if (DBG_TRUST)
-           log_debug ("Using flat format for TOFU database.\n");
-       }
-      else if (have_tofu_d)
-       {
-         opt.tofu_db_format = TOFU_DB_SPLIT;
-         if (DBG_TRUST)
-           log_debug ("Using split format for TOFU database.\n");
-       }
-      else
-       {
-         opt.tofu_db_format = TOFU_DB_FLAT;
-         if (DBG_TRUST)
-           log_debug ("Using flat format for TOFU database.\n");
-       }
-    }
-
-  return xmalloc_clear (sizeof (struct dbs));
-}
-
-/* Release all of the resources associated with a DB meta-handle.  */
-static void
-closedbs (struct dbs *dbs)
-{
-  if (dbs->db)
-    {
-      struct db *old_head = db_cache;
-      struct db *db;
-      int count;
-
-      /* Find the last DB.  */
-      for (db = dbs->db, count = 1; db->next; db = db->next, count ++)
-        {
-          /* When we leave batch mode we leave batch mode on any
-             cached connections.  */
-          if (! batch_update)
-            log_assert (! db->batch_update);
-        }
-      if (! batch_update)
-        log_assert (! db->batch_update);
-
-      /* Join the two lists.  */
-      db->next = db_cache;
-      if (db_cache)
-        db_cache->prevp = &db->next;
-
-      /* Update the (new) first element.  */
-      db_cache = dbs->db;
-      dbs->db->prevp = &db_cache;
-
-      db_cache_count += count;
-
-      /* Make sure that we don't have too many DBs on DB_CACHE.  If
-         so, free some.  */
-      if (db_cache_count > DB_CACHE_ENTRIES)
-        {
-          /* We need to find the (DB_CACHE_ENTRIES + 1)th entry.  It
-             is easy to skip the first COUNT entries since we still
-             have a handle on the old head.  */
-          int skip = DB_CACHE_ENTRIES - count;
-          while (-- skip > 0)
-            old_head = old_head->next;
-
-          *old_head->prevp = NULL;
-
-          while (old_head)
-            {
-              db = old_head->next;
-              closedb (old_head);
-              old_head = db;
-              db_cache_count --;
-            }
-        }
-    }
-
+  sqlite3_close (dbs->db);
   xfree (dbs);
-
-#if DEBUG_TOFU_CACHE
-  log_debug ("Queries: %d (prepares saved: %d)\n",
-             queries, prepares_saved);
-#endif
+  ctrl->tofu.dbs = NULL;
 }
 
 
@@ -1138,17 +785,12 @@ get_single_long_cb2 (void *cookie, int argc, char **argv, char **azColName,
 
    If SHOW_OLD is set, the binding's old policy is displayed.  */
 static gpg_error_t
-record_binding (struct dbs *dbs, const char *fingerprint, const char *email,
+record_binding (tofu_dbs_t dbs, const char *fingerprint, const char *email,
                const char *user_id, enum tofu_policy policy, int show_old)
 {
   char *fingerprint_pp = format_hexfingerprint (fingerprint, NULL, 0);
-  struct db *db_email = NULL, *db_key = NULL;
   gpg_error_t rc;
   char *err = NULL;
-  /* policy_old needs to be a long and not an enum tofu_policy,
-     because we pass it by reference to get_single_long_cb2, which
-     expects a long.  */
-  long policy_old = TOFU_POLICY_NONE;
 
   if (! (policy == TOFU_POLICY_AUTO
         || policy == TOFU_POLICY_GOOD
@@ -1157,89 +799,62 @@ record_binding (struct dbs *dbs, const char *fingerprint, const char *email,
         || policy == TOFU_POLICY_ASK))
     log_bug ("%s: Bad value for policy (%d)!\n", __func__, policy);
 
-  db_email = getdb (dbs, email, DB_EMAIL);
-  if (! db_email)
-    {
-      rc = gpg_error (GPG_ERR_GENERAL);
-      goto leave;
-    }
-
-  if (opt.tofu_db_format == TOFU_DB_SPLIT)
-    /* In the split format, we need to update two DBs.  To keep them
-       consistent, we start a transaction on each.  Note: this is the
-       only place where we start two transaction and we always start
-       transaction on the DB_KEY DB first, thus deadlock is not
-       possible.  */
-    {
-      db_key = getdb (dbs, fingerprint, DB_KEY);
-      if (! db_key)
-        {
-          rc = gpg_error (GPG_ERR_GENERAL);
-          goto leave;
-        }
-
-      rc = begin_transaction (db_email, 0);
-      if (rc)
-        goto leave;
 
-      rc = begin_transaction (db_key, 0);
-      if (rc)
-        goto out_revert_one;
-    }
-  else
+  if (DBG_TRUST || show_old)
     {
-      rc = begin_transaction (db_email, 1);
-      if (rc)
-        goto leave;
-    }
+      /* Get the old policy.  Since this is just for informational
+       * purposes, there is no need to start a transaction or to die
+       * if there is a failure.  */
 
+      /* policy_old needs to be a long and not an enum tofu_policy,
+         because we pass it by reference to get_single_long_cb2, which
+         expects a long.  */
+      long policy_old = TOFU_POLICY_NONE;
 
-  if (show_old)
-    /* Get the old policy.  Since this is just for informational
-       purposes, there is no need to start a transaction or to die if
-       there is a failure.  */
-    {
-      rc = sqlite3_stepx
-       (db_email->db, &db_email->s.record_binding_get_old_policy,
+      rc = gpgsql_stepx
+       (dbs->db, &dbs->s.record_binding_get_old_policy,
          get_single_long_cb2, &policy_old, &err,
         "select policy from bindings where fingerprint = ? and email = ?",
-        SQLITE_ARG_STRING, fingerprint, SQLITE_ARG_STRING, email,
-         SQLITE_ARG_END);
+        GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_STRING, email,
+         GPGSQL_ARG_END);
       if (rc)
        {
          log_debug ("TOFU: Error reading from binding database"
-                    " (reading policy for <%s, %s>): %s\n",
+                    " (reading policy for <key: %s, user id: %s>): %s\n",
                     fingerprint, email, err);
          sqlite3_free (err);
        }
-    }
 
-  if (DBG_TRUST)
-    {
       if (policy_old != TOFU_POLICY_NONE)
-       log_debug ("Changing TOFU trust policy for binding <%s, %s>"
-                  " from %s to %s.\n",
-                  fingerprint, email,
-                  tofu_policy_str (policy_old),
-                  tofu_policy_str (policy));
+        (show_old ? log_info : log_debug)
+          ("Changing TOFU trust policy for binding"
+           " <key: %s, user id: %s> from %s to %s.\n",
+           fingerprint, show_old ? user_id : email,
+           tofu_policy_str (policy_old),
+           tofu_policy_str (policy));
       else
-       log_debug ("Set TOFU trust policy for binding <%s, %s> to %s.\n",
-                  fingerprint, email,
-                  tofu_policy_str (policy));
-    }
+        (show_old ? log_info : log_debug)
+          ("Setting TOFU trust policy for new binding"
+           " <key: %s, user id: %s> to %s.\n",
+           fingerprint, show_old ? user_id : email,
+           tofu_policy_str (policy));
 
-  if (policy_old == policy)
-    /* Nothing to do.  */
-    goto out;
+      if (policy_old == policy)
+        {
+          rc = 0;
+          goto leave; /* Nothing to do.  */
+        }
+    }
 
   if (opt.dry_run)
     {
       log_info ("TOFU database update skipped due to --dry-run\n");
-      goto out;
+      rc = 0;
+      goto leave;
     }
 
-  rc = sqlite3_stepx
-    (db_email->db, &db_email->s.record_binding_update, NULL, NULL, &err,
+  rc = gpgsql_stepx
+    (dbs->db, &dbs->s.record_binding_update, NULL, NULL, &err,
      "insert or replace into bindings\n"
      " (oid, fingerprint, email, user_id, time, policy)\n"
      " values (\n"
@@ -1248,74 +863,21 @@ record_binding (struct dbs *dbs, const char *fingerprint, const char *email,
        based on the fingerprint and email since they are unique.  */
      "  (select oid from bindings where fingerprint = ? and email = ?),\n"
      "  ?, ?, ?, strftime('%s','now'), ?);",
-     SQLITE_ARG_STRING, fingerprint, SQLITE_ARG_STRING, email,
-     SQLITE_ARG_STRING, fingerprint, SQLITE_ARG_STRING, email,
-     SQLITE_ARG_STRING, user_id, SQLITE_ARG_INT, (int) policy,
-     SQLITE_ARG_END);
+     GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_STRING, email,
+     GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_STRING, email,
+     GPGSQL_ARG_STRING, user_id, GPGSQL_ARG_INT, (int) policy,
+     GPGSQL_ARG_END);
   if (rc)
     {
       log_error (_("error updating TOFU database: %s\n"), err);
-      print_further_info (" insert bindings <%s, %s> = %s",
+      print_further_info (" insert bindings <key: %s, user id: %s> = %s",
                           fingerprint, email, tofu_policy_str (policy));
       sqlite3_free (err);
-      goto out;
-    }
-
-  if (db_key)
-    /* We also need to update the key DB.  */
-    {
-      log_assert (opt.tofu_db_format == TOFU_DB_SPLIT);
-
-      rc = sqlite3_stepx
-       (db_key->db, &db_key->s.record_binding_update2, NULL, NULL, &err,
-        "insert or replace into bindings\n"
-        " (oid, fingerprint, email, user_id)\n"
-        " values (\n"
-        /* If we don't explicitly reuse the OID, then SQLite will
-           reallocate a new one.  We just need to search for the OID
-           based on the fingerprint and email since they are unique.  */
-        "  (select oid from bindings where fingerprint = ? and email = ?),\n"
-        "  ?, ?, ?);",
-        SQLITE_ARG_STRING, fingerprint, SQLITE_ARG_STRING, email,
-         SQLITE_ARG_STRING, fingerprint, SQLITE_ARG_STRING, email,
-         SQLITE_ARG_STRING, user_id, SQLITE_ARG_END);
-      if (rc)
-       {
-         log_error (_("error updating TOFU database: %s\n"), err);
-          print_further_info ("insert bindings <%s, %s>",
-                              fingerprint, email);
-         sqlite3_free (err);
-         goto out;
-       }
-    }
-  else
-    log_assert (opt.tofu_db_format == TOFU_DB_FLAT);
-
- out:
-  if (opt.tofu_db_format == TOFU_DB_SPLIT)
-    /* We only need a transaction for the split format.  */
-    {
-      gpg_error_t rc2;
-
-      if (rc)
-        rc2 = rollback_transaction (db_key);
-      else
-        rc2 = end_transaction (db_key, 0);
-      if (rc2)
-        sqlite3_free (err);
-
-    out_revert_one:
-      if (rc)
-        rc2 = rollback_transaction (db_email);
-      else
-        rc2 = end_transaction (db_email, 0);
-      if (rc2)
-        sqlite3_free (err);
+      goto leave;
     }
 
  leave:
   xfree (fingerprint_pp);
-
   return rc;
 }
 
@@ -1375,6 +937,10 @@ struct signature_stats
   /* Number of signatures during this time.  */
   unsigned long count;
 
+  /* If the corresponding key/user id has been expired / revoked.  */
+  int is_expired;
+  int is_revoked;
+
   /* The key that generated this signature.  */
   char fingerprint[1];
 };
@@ -1398,7 +964,7 @@ signature_stats_prepend (struct signature_stats **statsp,
                         unsigned long count)
 {
   struct signature_stats *stats =
-    xmalloc (sizeof (*stats) + strlen (fingerprint));
+    xmalloc_clear (sizeof (*stats) + strlen (fingerprint));
 
   stats->next = *statsp;
   *statsp = stats;
@@ -1481,31 +1047,26 @@ time_ago_scale (signed long t)
    if CONFLICT is not NULL.  Returns _tofu_GET_POLICY_ERROR if an error
    occurs.  */
 static enum tofu_policy
-get_policy (struct dbs *dbs, const char *fingerprint, const char *email,
+get_policy (tofu_dbs_t dbs, const char *fingerprint, const char *email,
            char **conflict)
 {
-  struct db *db;
   int rc;
   char *err = NULL;
   strlist_t strlist = NULL;
   enum tofu_policy policy = _tofu_GET_POLICY_ERROR;
   long along;
 
-  db = getdb (dbs, email, DB_EMAIL);
-  if (! db)
-    return _tofu_GET_POLICY_ERROR;
-
   /* Check if the <FINGERPRINT, EMAIL> binding is known
      (TOFU_POLICY_NONE cannot appear in the DB.  Thus, if POLICY is
      still TOFU_POLICY_NONE after executing the query, then the
      result set was empty.)  */
-  rc = sqlite3_stepx (db->db, &db->s.get_policy_select_policy_and_conflict,
+  rc = gpgsql_stepx (dbs->db, &dbs->s.get_policy_select_policy_and_conflict,
                       strings_collect_cb2, &strlist, &err,
                       "select policy, conflict from bindings\n"
                       " where fingerprint = ? and email = ?",
-                      SQLITE_ARG_STRING, fingerprint,
-                      SQLITE_ARG_STRING, email,
-                      SQLITE_ARG_END);
+                      GPGSQL_ARG_STRING, fingerprint,
+                      GPGSQL_ARG_STRING, email,
+                      GPGSQL_ARG_END);
   if (rc)
     {
       log_error (_("error reading TOFU database: %s\n"), err);
@@ -1558,7 +1119,7 @@ get_policy (struct dbs *dbs, const char *fingerprint, const char *email,
 
   /* If CONFLICT is set, then policy should be TOFU_POLICY_ASK.  But,
      just in case, we do the check again here and ignore the conflict
-     is POLICY is not TOFU_POLICY_ASK.  */
+     if POLICY is not TOFU_POLICY_ASK.  */
   if (conflict)
     {
       if (policy == TOFU_POLICY_ASK && *strlist->next->d)
@@ -1581,33 +1142,544 @@ get_policy (struct dbs *dbs, const char *fingerprint, const char *email,
   return policy;
 }
 
-/* Return the trust level (TRUST_NEVER, etc.) for the binding
-   <FINGERPRINT, EMAIL> (email is already normalized).  If no policy
-   is registered, returns TOFU_POLICY_NONE.  If an error occurs,
-   returns _tofu_GET_TRUST_ERROR.
 
-   USER_ID is the unadultered user id.
+/* Format the first part of a conflict message and return that as a
+ * malloced string.  */
+static char *
+format_conflict_msg_part1 (int policy, const char *conflict,
+                           const char *fingerprint, const char *email)
+{
+  estream_t fp;
+  char *binding;
+  int binding_shown = 0;
+  char *tmpstr, *text;
+
+  binding = xasprintf ("<%s, %s>", fingerprint, email);
+
+  fp = es_fopenmem (0, "rw,samethread");
+  if (!fp)
+    log_fatal ("error creating memory stream: %s\n",
+               gpg_strerror (gpg_error_from_syserror()));
+
+  if (policy == TOFU_POLICY_NONE)
+    {
+      es_fprintf (fp, _("The binding %s is NOT known."), binding);
+      es_fputs ("  ", fp);
+      binding_shown = 1;
+    }
+  else if (policy == TOFU_POLICY_ASK
+           /* If there the conflict is with itself, then don't
+            * display this message.  */
+           && conflict && strcmp (conflict, fingerprint))
+    {
+      es_fprintf (fp,
+                  _("The key with fingerprint %s raised a conflict "
+                    "with the binding %s."
+                    "  Since this binding's policy was 'auto', it was "
+                    "changed to 'ask'."),
+                  conflict, binding);
+      es_fputs ("  ", fp);
+      binding_shown = 1;
+    }
+
+  /* TRANSLATORS: The %s%s is replaced by either a fingerprint and a
+     blank or by two empty strings.  */
+  es_fprintf (fp,
+              _("Please indicate whether you believe the binding %s%s"
+                "is legitimate (the key belongs to the stated owner) "
+                "or a forgery (bad)."),
+              binding_shown ? "" : binding,
+              binding_shown ? "" : " ");
+  es_fputc ('\n', fp);
+
+  xfree (binding);
+
+  es_fputc (0, fp);
+  if (es_fclose_snatch (fp, (void **)&tmpstr, NULL))
+    log_fatal ("error snatching memory stream\n");
+  text = format_text (tmpstr, 0, 72, 80);
+  es_free (tmpstr);
+
+  return text;
+}
+
+
+/* Ask the user about the binding.  There are three ways we could end
+ * up here:
+ *
+ *   - This is a new binding and there is a conflict
+ *     (policy == TOFU_POLICY_NONE && bindings_with_this_email_count > 0),
+ *
+ *   - This is a new binding and opt.tofu_default_policy is set to
+ *     ask.  (policy == TOFU_POLICY_NONE && opt.tofu_default_policy ==
+ *     TOFU_POLICY_ASK), or,
+ *
+ *   - The policy is ask (the user deferred last time) (policy ==
+ *     TOFU_POLICY_ASK).
+ *
+ * Note: this function must not be called while in a transaction!
+ */
+static void
+ask_about_binding (ctrl_t ctrl,
+                   enum tofu_policy *policy,
+                   int *trust_level,
+                   int bindings_with_this_email_count,
+                   strlist_t bindings_with_this_email,
+                   char *conflict,
+                   const char *fingerprint,
+                   const char *email,
+                   const char *user_id)
+{
+  tofu_dbs_t dbs;
+  char *sqerr = NULL;
+  int rc;
+  estream_t fp;
+  strlist_t other_user_ids = NULL;
+  struct signature_stats *stats = NULL;
+  struct signature_stats *stats_iter = NULL;
+  char *prompt;
+  char *choices;
+
+  dbs = ctrl->tofu.dbs;
+  log_assert (dbs);
+  log_assert (dbs->in_transaction == 0);
+
+  fp = es_fopenmem (0, "rw,samethread");
+  if (!fp)
+    log_fatal ("error creating memory stream: %s\n",
+               gpg_strerror (gpg_error_from_syserror()));
+
+  {
+    char *text = format_conflict_msg_part1 (*policy, conflict,
+                                            fingerprint, email);
+    es_fputs (text, fp);
+    es_fputc ('\n', fp);
+    xfree (text);
+  }
+
+  begin_transaction (ctrl, 0);
+
+  /* Find other user ids associated with this key and whether the
+   * bindings are marked as good or bad.  */
+  rc = gpgsql_stepx
+    (dbs->db, &dbs->s.get_trust_gather_other_user_ids,
+     strings_collect_cb2, &other_user_ids, &sqerr,
+     "select user_id, policy from bindings where fingerprint = ?;",
+     GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_END);
+  if (rc)
+    {
+      log_error (_("error gathering other user IDs: %s\n"), sqerr);
+      sqlite3_free (sqerr);
+      sqerr = NULL;
+    }
+
+  if (other_user_ids)
+    {
+      strlist_t strlist_iter;
+
+      es_fprintf (fp, _("Known user IDs associated with this key:\n"));
+      for (strlist_iter = other_user_ids;
+           strlist_iter;
+           strlist_iter = strlist_iter->next)
+        {
+          char *other_user_id = strlist_iter->d;
+          char *other_thing;
+          enum tofu_policy other_policy;
+
+          log_assert (strlist_iter->next);
+          strlist_iter = strlist_iter->next;
+          other_thing = strlist_iter->d;
+
+          other_policy = atoi (other_thing);
+
+          es_fprintf (fp, "  %s (", other_user_id);
+          es_fprintf (fp, _("policy: %s"), tofu_policy_str (other_policy));
+          es_fprintf (fp, ")\n");
+        }
+      es_fprintf (fp, "\n");
+
+      free_strlist (other_user_ids);
+    }
+
+  /* Find other keys associated with this email address.  */
+  /* FIXME: When generating the statistics, do we want the time
+     embedded in the signature (column 'sig_time') or the time that
+     we first verified the signature (column 'time').  */
+  rc = gpgsql_stepx
+    (dbs->db, &dbs->s.get_trust_gather_other_keys,
+     signature_stats_collect_cb, &stats, &sqerr,
+     "select fingerprint, policy, time_ago, count(*)\n"
+     " from (select bindings.*,\n"
+     "        case\n"
+     /* From the future (but if its just a couple of hours in the
+      * future don't turn it into a warning)?  Or should we use
+      * small, medium or large units?  (Note: whatever we do, we
+      * keep the value in seconds.  Then when we group, everything
+      * that rounds to the same number of seconds is grouped.)  */
+     "         when delta < -("STRINGIFY (TIME_AGO_FUTURE_IGNORE)") then -1\n"
+     "         when delta < ("STRINGIFY (TIME_AGO_MEDIUM_THRESHOLD)")\n"
+     "          then max(0,\n"
+     "                   round(delta / ("STRINGIFY (TIME_AGO_UNIT_SMALL)"))\n"
+     "               * ("STRINGIFY (TIME_AGO_UNIT_SMALL)"))\n"
+     "         when delta < ("STRINGIFY (TIME_AGO_LARGE_THRESHOLD)")\n"
+     "          then round(delta / ("STRINGIFY (TIME_AGO_UNIT_MEDIUM)"))\n"
+     "               * ("STRINGIFY (TIME_AGO_UNIT_MEDIUM)")\n"
+     "         else round(delta / ("STRINGIFY (TIME_AGO_UNIT_LARGE)"))\n"
+     "              * ("STRINGIFY (TIME_AGO_UNIT_LARGE)")\n"
+     "        end time_ago,\n"
+     "        delta time_ago_raw\n"
+     "       from bindings\n"
+     "       left join\n"
+     "         (select *,\n"
+     "            cast(strftime('%s','now') - sig_time as real) delta\n"
+     "           from signatures) ss\n"
+     "        on ss.binding = bindings.oid)\n"
+     " where email = ?\n"
+     " group by fingerprint, time_ago\n"
+     /* Make sure the current key is first.  */
+     " order by fingerprint = ? asc, fingerprint desc, time_ago desc;\n",
+     GPGSQL_ARG_STRING, email, GPGSQL_ARG_STRING, fingerprint,
+     GPGSQL_ARG_END);
+  if (rc)
+    {
+      strlist_t strlist_iter;
+
+      log_error (_("error gathering signature stats: %s\n"), sqerr);
+      sqlite3_free (sqerr);
+      sqerr = NULL;
+
+      es_fprintf (fp, ngettext("The email address \"%s\" is"
+                               " associated with %d key:\n",
+                               "The email address \"%s\" is"
+                               " associated with %d keys:\n",
+                               bindings_with_this_email_count),
+                  email, bindings_with_this_email_count);
+      for (strlist_iter = bindings_with_this_email;
+           strlist_iter;
+           strlist_iter = strlist_iter->next)
+        es_fprintf (fp, "  %s\n", strlist_iter->d);
+    }
+  else
+    {
+      KEYDB_HANDLE hd;
+      char *key = NULL;
+
+      if (! stats || strcmp (stats->fingerprint, fingerprint))
+        {
+          /* If we have already added this key to the DB, then it will
+           * be first (see the above select).  Since the first key on
+           * the list is not this key, we must not yet have verified any
+           * messages signed by this key.  Add a dummy entry.  */
+          signature_stats_prepend (&stats, fingerprint, TOFU_POLICY_AUTO, 0, 0);
+        }
+
+      /* Figure out which user ids are revoked or expired.  */
+      hd = keydb_new ();
+      for (stats_iter = stats; stats_iter; stats_iter = stats_iter->next)
+        {
+          KEYDB_SEARCH_DESC desc;
+          kbnode_t kb;
+          PKT_public_key *pk;
+          kbnode_t n;
+          int found_user_id;
+
+          rc = keydb_search_reset (hd);
+          if (rc)
+            {
+              log_error (_("resetting keydb: %s\n"),
+                         gpg_strerror (rc));
+              continue;
+            }
+
+          rc = classify_user_id (stats_iter->fingerprint, &desc, 0);
+          if (rc)
+            {
+              log_error (_("error parsing key specification '%s': %s\n"),
+                         stats_iter->fingerprint, gpg_strerror (rc));
+              continue;
+            }
+
+          rc = keydb_search (hd, &desc, 1, NULL);
+          if (rc)
+            {
+              log_error (_("key \"%s\" not found: %s\n"),
+                         stats_iter->fingerprint,
+                         gpg_strerror (rc));
+              continue;
+            }
+
+          rc = keydb_get_keyblock (hd, &kb);
+          if (rc)
+            {
+              log_error (_("error reading keyblock: %s\n"),
+                         gpg_strerror (rc));
+              print_further_info ("fingerprint: %s", stats_iter->fingerprint);
+              continue;
+            }
+
+          merge_keys_and_selfsig (kb);
+
+          log_assert (kb->pkt->pkttype == PKT_PUBLIC_KEY);
+          pk = kb->pkt->pkt.public_key;
+
+          if (pk->has_expired)
+            stats_iter->is_expired = 1;
+          if (pk->flags.revoked)
+            stats_iter->is_revoked = 1;
+
+          n = kb;
+          found_user_id = 0;
+          while ((n = find_next_kbnode (n, PKT_USER_ID)) && ! found_user_id)
+            {
+              PKT_user_id *user_id2 = n->pkt->pkt.user_id;
+              char *email2;
+
+              if (user_id2->attrib_data)
+                continue;
+
+              email2 = email_from_user_id (user_id2->name);
+
+              if (strcmp (email, email2) == 0)
+                {
+                  found_user_id = 1;
+
+                  if (user_id2->is_revoked)
+                    stats_iter->is_revoked = 1;
+                  if (user_id2->is_expired)
+                    stats_iter->is_expired = 1;
+                }
+
+              xfree (email2);
+            }
+          release_kbnode (kb);
+
+          if (! found_user_id)
+            log_info (_("TOFU db may be corrupted: user id (%s)"
+                        " not on key block (%s)\n"),
+                      email, fingerprint);
+        }
+      keydb_release (hd);
+
+      es_fprintf (fp, _("Statistics for keys with the email address \"%s\":\n"),
+                  email);
+      for (stats_iter = stats; stats_iter; stats_iter = stats_iter->next)
+        {
+          if (! key || strcmp (key, stats_iter->fingerprint))
+            {
+              int this_key;
+              char *key_pp;
+
+              key = stats_iter->fingerprint;
+              this_key = strcmp (key, fingerprint) == 0;
+              key_pp = format_hexfingerprint (key, NULL, 0);
+              es_fprintf (fp, "  %s (", key_pp);
+
+              if (stats_iter->is_revoked)
+                {
+                  es_fprintf (fp, _("revoked"));
+                  es_fprintf (fp, _(", "));
+                }
+              else if (stats_iter->is_expired)
+                {
+                  es_fprintf (fp, _("expired"));
+                  es_fprintf (fp, _(", "));
+                }
+
+              if (this_key)
+                es_fprintf (fp, _("this key"));
+              else
+                es_fprintf (fp, _("policy: %s"),
+                            tofu_policy_str (stats_iter->policy));
+              es_fputs ("):\n", fp);
+              xfree (key_pp);
+            }
+
+          es_fputs ("    ", fp);
+          if (stats_iter->time_ago == -1)
+            es_fprintf (fp, ngettext("%ld message signed in the future.",
+                                     "%ld messages signed in the future.",
+                                     stats_iter->count), stats_iter->count);
+          else
+            {
+              long t_scaled = time_ago_scale (stats_iter->time_ago);
+
+              /* TANSLATORS: This string is concatenated with one of
+               * the day/week/month strings to form one sentence.  */
+              es_fprintf (fp, ngettext("%ld message signed",
+                                       "%ld messages signed",
+                                       stats_iter->count), stats_iter->count);
+              if (!stats_iter->count)
+                es_fputs (".", fp);
+              else if (stats_iter->time_ago < TIME_AGO_UNIT_MEDIUM)
+                es_fprintf (fp, ngettext(" over the past %ld day.",
+                                         " over the past %ld days.",
+                                         t_scaled), t_scaled);
+              else if (stats_iter->time_ago < TIME_AGO_UNIT_LARGE)
+                es_fprintf (fp, ngettext(" over the past %ld week.",
+                                         " over the past %ld weeks.",
+                                         t_scaled), t_scaled);
+              else
+                es_fprintf (fp, ngettext(" over the past %ld month.",
+                                         " over the past %ld months.",
+                                         t_scaled), t_scaled);
+            }
+          es_fputs ("\n", fp);
+        }
+    }
+
+  end_transaction (ctrl, 0);
+
+  if ((*policy == TOFU_POLICY_NONE && bindings_with_this_email_count > 0)
+      || (*policy == TOFU_POLICY_ASK
+          && (conflict || bindings_with_this_email_count > 0)))
+    {
+      /* This is a conflict.  */
+
+      /* TRANSLATORS: Please translate the text found in the source
+       * file below.  We don't directly internationalize that text so
+       * that we can tweak it without breaking translations.  */
+      char *text = _("TOFU detected a binding conflict");
+      char *textbuf;
+      if (!strcmp (text, "TOFU detected a binding conflict"))
+        {
+          /* No translation.  Use the English text.  */
+          text =
+            "Normally, there is only a single key associated with an email "
+            "address.  However, people sometimes generate a new key if "
+            "their key is too old or they think it might be compromised.  "
+            "Alternatively, a new key may indicate a man-in-the-middle "
+            "attack!  Before accepting this key, you should talk to or "
+            "call the person to make sure this new key is legitimate.";
+        }
+      textbuf = format_text (text, 0, 72, 80);
+      es_fprintf (fp, "\n%s\n", textbuf);
+      xfree (textbuf);
+    }
+
+  es_fputc ('\n', fp);
+
+  /* Add a NUL terminator.  */
+  es_fputc (0, fp);
+  if (es_fclose_snatch (fp, (void **) &prompt, NULL))
+    log_fatal ("error snatching memory stream\n");
+
+  /* I think showing the large message once is sufficient.  If we
+   * would move it right before the cpr_get many lines will scroll
+   * away and the user might not realize that he merely entered a
+   * wrong choise (because he does not see that either).  As a small
+   * benefit we allow C-L to redisplay everything.  */
+  tty_printf ("%s", prompt);
+
+  /* Suspend any transaction: it could take a while until the user
+     responds.  */
+  tofu_suspend_batch_transaction (ctrl);
+  while (1)
+    {
+      char *response;
+
+      /* TRANSLATORS: Two letters (normally the lower and upper case
+       * version of the hotkey) for each of the five choices.  If
+       * there is only one choice in your language, repeat it.  */
+      choices = _("gG" "aA" "uU" "rR" "bB");
+      if (strlen (choices) != 10)
+        log_bug ("Bad TOFU conflict translation!  Please report.");
+
+      response = cpr_get
+        ("tofu.conflict",
+         _("(G)ood, (A)ccept once, (U)nknown, (R)eject once, (B)ad? "));
+      trim_spaces (response);
+      cpr_kill_prompt ();
+      if (*response == CONTROL_L)
+        tty_printf ("%s", prompt);
+      else if (strlen (response) == 1)
+        {
+          char *choice = strchr (choices, *response);
+          if (choice)
+            {
+              int c = ((size_t) choice - (size_t) choices) / 2;
+
+              switch (c)
+                {
+                case 0: /* Good.  */
+                  *policy = TOFU_POLICY_GOOD;
+                  *trust_level = tofu_policy_to_trust_level (*policy);
+                  break;
+                case 1: /* Accept once.  */
+                  *policy = TOFU_POLICY_ASK;
+                  *trust_level = tofu_policy_to_trust_level (TOFU_POLICY_GOOD);
+                  break;
+                case 2: /* Unknown.  */
+                  *policy = TOFU_POLICY_UNKNOWN;
+                  *trust_level = tofu_policy_to_trust_level (*policy);
+                  break;
+                case 3: /* Reject once.  */
+                  *policy = TOFU_POLICY_ASK;
+                  *trust_level = tofu_policy_to_trust_level (TOFU_POLICY_BAD);
+                  break;
+                case 4: /* Bad.  */
+                  *policy = TOFU_POLICY_BAD;
+                  *trust_level = tofu_policy_to_trust_level (*policy);
+                  break;
+                default:
+                  log_bug ("c should be between 0 and 4 but it is %d!", c);
+                }
+
+              if (record_binding (dbs, fingerprint, email, user_id,
+                                  *policy, 0))
+                {
+                  /* If there's an error registering the
+                   * binding, don't save the signature.  */
+                  *trust_level = _tofu_GET_TRUST_ERROR;
+                }
+              break;
+            }
+        }
+      xfree (response);
+    }
+  tofu_resume_batch_transaction (ctrl);
+
+  xfree (prompt);
 
-   If MAY_ASK is set, then we may interact with the user.  This is
-   necessary if there is a conflict or the binding's policy is
-   TOFU_POLICY_ASK.  In the case of a conflict, we set the new
-   conflicting binding's policy to TOFU_POLICY_ASK.  In either case,
-   we return TRUST_UNDEFINED.  */
+  signature_stats_free (stats);
+}
+
+
+/* Return the trust level (TRUST_NEVER, etc.) for the binding
+ * <FINGERPRINT, EMAIL> (email is already normalized).  If no policy
+ * is registered, returns TOFU_POLICY_NONE.  If an error occurs,
+ * returns _tofu_GET_TRUST_ERROR.
+ *
+ * PK is the public key object for FINGERPRINT.
+ *
+ * USER_ID is the unadulterated user id.
+ *
+ * If MAY_ASK is set, then we may interact with the user.  This is
+ * necessary if there is a conflict or the binding's policy is
+ * TOFU_POLICY_ASK.  In the case of a conflict, we set the new
+ * conflicting binding's policy to TOFU_POLICY_ASK.  In either case,
+ * we return TRUST_UNDEFINED.  Note: if MAY_ASK is set, then this
+ * function must not be called while in a transaction!  */
 static enum tofu_policy
-get_trust (struct dbs *dbs, const char *fingerprint, const char *email,
+get_trust (ctrl_t ctrl, PKT_public_key *pk,
+           const char *fingerprint, const char *email,
           const char *user_id, int may_ask)
 {
-  char *fingerprint_pp;
-  struct db *db;
+  tofu_dbs_t dbs = ctrl->tofu.dbs;
+  int in_transaction = 0;
   enum tofu_policy policy;
   char *conflict = NULL;
   int rc;
-  char *err = NULL;
+  char *sqerr = NULL;
   strlist_t bindings_with_this_email = NULL;
   int bindings_with_this_email_count;
   int change_conflicting_to_ask = 0;
   int trust_level = TRUST_UNKNOWN;
 
+  log_assert (dbs);
+
+  if (may_ask)
+    log_assert (dbs->in_transaction == 0);
+
   if (opt.batch)
     may_ask = 0;
 
@@ -1621,75 +1693,41 @@ get_trust (struct dbs *dbs, const char *fingerprint, const char *email,
               && _tofu_GET_TRUST_ERROR != TRUST_FULLY
               && _tofu_GET_TRUST_ERROR != TRUST_ULTIMATE);
 
-  db = getdb (dbs, email, DB_EMAIL);
-  if (! db)
-    return _tofu_GET_TRUST_ERROR;
-
-  fingerprint_pp = format_hexfingerprint (fingerprint, NULL, 0);
+  begin_transaction (ctrl, 0);
+  in_transaction = 1;
 
   policy = get_policy (dbs, fingerprint, email, &conflict);
-  if (policy == TOFU_POLICY_AUTO || policy == TOFU_POLICY_NONE)
+  {
     /* See if the key is ultimately trusted.  If so, we're done.  */
-    {
-      PKT_public_key *pk;
-      u32 kid[2];
-      char fpr_bin[MAX_FINGERPRINT_LEN+1];
-      size_t fpr_bin_len;
-
-      if (!hex2str (fingerprint, fpr_bin, sizeof fpr_bin, &fpr_bin_len))
-        {
-          log_error ("error converting fingerprint: %s\n",
-                     gpg_strerror (gpg_error_from_syserror ()));
-          return _tofu_GET_TRUST_ERROR;
-        }
+    u32 kid[2];
 
-      /* We need to lookup the key by fingerprint again so that we can
-         properly extract the keyid.  Extracting direct from the
-         fingerprint works only for v4 keys and would assume that
-         there is no collision in the low 64 bit.  We can't guarantee
-         the latter in case the Tofu DB is used with a different
-         keyring.  In any case the UTK stuff needs to be changed to
-         use only fingerprints.  */
-      pk = xtrycalloc (1, sizeof *pk);
-      if (!pk)
-         {
-           log_error (_("out of core\n"));
-           return _tofu_GET_TRUST_ERROR;
-         }
-      rc = get_pubkey_byfprint_fast (pk, fpr_bin, fpr_bin_len);
-      if (rc)
-        {
-          log_error (_("public key %s not found: %s\n"),
-                     fingerprint, gpg_strerror (rc));
-          return _tofu_GET_TRUST_ERROR;
-        }
-      keyid_from_pk (pk, kid);
-      free_public_key (pk);
+    keyid_from_pk (pk, kid);
 
-      if (tdb_keyid_is_utk (kid))
-        {
-          if (policy == TOFU_POLICY_NONE)
-            {
-              if (record_binding (dbs, fingerprint, email, user_id,
-                                  TOFU_POLICY_AUTO, 0) != 0)
-                {
-                  log_error (_("error setting TOFU binding's trust level"
-                               " to %s\n"), "auto");
-                  trust_level = _tofu_GET_TRUST_ERROR;
-                  goto out;
-                }
-            }
+    if (tdb_keyid_is_utk (kid))
+      {
+        if (policy == TOFU_POLICY_NONE)
+          {
+            if (record_binding (dbs, fingerprint, email, user_id,
+                                TOFU_POLICY_AUTO, 0) != 0)
+              {
+                log_error (_("error setting TOFU binding's trust level"
+                             " to %s\n"), "auto");
+                trust_level = _tofu_GET_TRUST_ERROR;
+                goto out;
+              }
+          }
 
-          trust_level = TRUST_ULTIMATE;
-          goto out;
-        }
-    }
+        trust_level = TRUST_ULTIMATE;
+        goto out;
+      }
+  }
 
   if (policy == TOFU_POLICY_AUTO)
     {
       policy = opt.tofu_default_policy;
       if (DBG_TRUST)
-       log_debug ("TOFU: binding <%s, %s>'s policy is auto (default: %s).\n",
+       log_debug ("TOFU: binding <key: %s, user id: %s>'s policy is "
+                   " auto (default: %s).\n",
                   fingerprint, email,
                   tofu_policy_str (opt.tofu_default_policy));
     }
@@ -1700,9 +1738,9 @@ get_trust (struct dbs *dbs, const char *fingerprint, const char *email,
     case TOFU_POLICY_UNKNOWN:
     case TOFU_POLICY_BAD:
       /* The saved judgement is auto -> auto, good, unknown or bad.
-        We don't need to ask the user anything.  */
+       * We don't need to ask the user anything.  */
       if (DBG_TRUST)
-       log_debug ("TOFU: Known binding <%s, %s>'s policy: %s\n",
+       log_debug ("TOFU: Known binding <key: %s, user id: %s>'s policy: %s\n",
                   fingerprint, email, tofu_policy_str (policy));
       trust_level = tofu_policy_to_trust_level (policy);
       goto out;
@@ -1719,7 +1757,7 @@ get_trust (struct dbs *dbs, const char *fingerprint, const char *email,
 
     case TOFU_POLICY_NONE:
       /* The binding is new, we need to check for conflicts.  Case #3
-        below.  */
+       * below.  */
       break;
 
     case _tofu_GET_POLICY_ERROR:
@@ -1732,56 +1770,58 @@ get_trust (struct dbs *dbs, const char *fingerprint, const char *email,
 
 
   /* We get here if:
-
-       1. The saved policy is auto and the default policy is ask
-          (get_policy() == TOFU_POLICY_AUTO
-           && opt.tofu_default_policy == TOFU_POLICY_ASK)
-
-       2. The saved policy is ask (either last time the user selected
-          accept once or reject once or there was a conflict and this
-          binding's policy was changed from auto to ask)
-         (policy == TOFU_POLICY_ASK), or,
-
-       3. We don't have a saved policy (policy == TOFU_POLICY_NONE)
-          (need to check for a conflict).
+   *
+   *   1. The saved policy is auto and the default policy is ask
+   *      (get_policy() == TOFU_POLICY_AUTO
+   *       && opt.tofu_default_policy == TOFU_POLICY_ASK)
+   *
+   *   2. The saved policy is ask (either last time the user selected
+   *      accept once or reject once or there was a conflict and this
+   *      binding's policy was changed from auto to ask)
+   *      (policy == TOFU_POLICY_ASK), or,
+   *
+   *   3. We don't have a saved policy (policy == TOFU_POLICY_NONE)
+   *      (need to check for a conflict).
    */
 
   /* Look for conflicts.  This is needed in all 3 cases.
-
-     Get the fingerprints of any bindings that share the email
-     address.  Note: if the binding in question is in the DB, it will
-     also be returned.  Thus, if the result set is empty, then this is
-     a new binding.  */
-  rc = sqlite3_stepx
-    (db->db, &db->s.get_trust_bindings_with_this_email,
-     strings_collect_cb2, &bindings_with_this_email, &err,
+   *
+   * Get the fingerprints of any bindings that share the email
+   * address.  Note: if the binding in question is in the DB, it will
+   * also be returned.  Thus, if the result set is empty, then this is
+   * a new binding.  */
+  rc = gpgsql_stepx
+    (dbs->db, &dbs->s.get_trust_bindings_with_this_email,
+     strings_collect_cb2, &bindings_with_this_email, &sqerr,
      "select distinct fingerprint from bindings where email = ?;",
-     SQLITE_ARG_STRING, email, SQLITE_ARG_END);
+     GPGSQL_ARG_STRING, email, GPGSQL_ARG_END);
   if (rc)
     {
-      log_error (_("error reading TOFU database: %s\n"), err);
+      log_error (_("error reading TOFU database: %s\n"), sqerr);
       print_further_info ("listing fingerprints");
-      sqlite3_free (err);
+      sqlite3_free (sqerr);
       goto out;
     }
 
   bindings_with_this_email_count = strlist_length (bindings_with_this_email);
   if (bindings_with_this_email_count == 0
       && opt.tofu_default_policy != TOFU_POLICY_ASK)
-    /* New binding with no conflict and a concrete default policy.
-
-       We've never observed a binding with this email address
-       (BINDINGS_WITH_THIS_EMAIL_COUNT is 0 and the above query would return
-       the current binding if it were in the DB) and we have a default
-       policy, which is not to ask the user.  */
     {
+      /* New binding with no conflict and a concrete default policy.
+       *
+       * We've never observed a binding with this email address
+       * BINDINGS_WITH_THIS_EMAIL_COUNT is 0 and the above query would
+       * return the current binding if it were in the DB) and we have
+       * a default policy, which is not to ask the user.
+       */
+
       /* If we've seen this binding, then we've seen this email and
         policy couldn't possibly be TOFU_POLICY_NONE.  */
       log_assert (policy == TOFU_POLICY_NONE);
 
       if (DBG_TRUST)
-       log_debug ("TOFU: New binding <%s, %s>, no conflict.\n",
-                  email, fingerprint);
+       log_debug ("TOFU: New binding <key: %s, user id: %s>, no conflict.\n",
+                  fingerprint, email);
 
       if (record_binding (dbs, fingerprint, email, user_id,
                          TOFU_POLICY_AUTO, 0) != 0)
@@ -1797,18 +1837,20 @@ get_trust (struct dbs *dbs, const char *fingerprint, const char *email,
     }
 
   if (policy == TOFU_POLICY_NONE)
-    /* This is a new binding and we have a conflict.  Mark any
-       conflicting bindings that have an automatic policy as now
-       requiring confirmation.  Note: we delay this until after we ask
-       for confirmation so that when the current policy is printed, it
-       is correct.  */
-    change_conflicting_to_ask = 1;
+    {
+      /* This is a new binding and we have a conflict.  Mark any
+       * conflicting bindings that have an automatic policy as now
+       * requiring confirmation.  Note: we delay this until after we
+       * ask for confirmation so that when the current policy is
+       * printed, it is correct.  */
+      change_conflicting_to_ask = 1;
+    }
 
   if (! may_ask)
-    /* We can only get here in the third case (no saved policy) and if
-       there is a conflict.  (If the policy was ask (cases #1 and #2)
-       and we weren't allowed to ask, we'd have already exited).  */
     {
+      /* We can only get here in the third case (no saved policy) and
+       * if there is a conflict.  (If the policy was ask (cases #1 and
+       * #2) and we weren't allowed to ask, we'd have already exited).  */
       log_assert (policy == TOFU_POLICY_NONE);
 
       if (record_binding (dbs, fingerprint, email, user_id,
@@ -1820,411 +1862,59 @@ get_trust (struct dbs *dbs, const char *fingerprint, const char *email,
       goto out;
     }
 
-  /* If we get here, we need to ask the user about the binding.  There
-     are three ways we could end up here:
+  /* We can't be in a normal transaction in ask_about_binding.  */
+  end_transaction (ctrl, 0);
+  in_transaction = 0;
 
-       - This is a new binding and there is a conflict
-         (policy == TOFU_POLICY_NONE && bindings_with_this_email_count > 0),
+  /* If we get here, we need to ask the user about the binding.  */
+  ask_about_binding (ctrl,
+                     &policy,
+                     &trust_level,
+                     bindings_with_this_email_count,
+                     bindings_with_this_email,
+                     conflict,
+                     fingerprint,
+                     email,
+                     user_id);
 
-       - This is a new binding and opt.tofu_default_policy is set to
-         ask.  (policy == TOFU_POLICY_NONE && opt.tofu_default_policy ==
-         TOFU_POLICY_ASK), or,
+ out:
+  if (in_transaction)
+    end_transaction (ctrl, 0);
 
-       - The policy is ask (the user deferred last time) (policy ==
-         TOFU_POLICY_ASK).
-   */
-  {
-    int is_conflict =
-      ((policy == TOFU_POLICY_NONE && bindings_with_this_email_count > 0)
-       || (policy == TOFU_POLICY_ASK && conflict));
-    estream_t fp;
-    strlist_t other_user_ids = NULL;
-    struct signature_stats *stats = NULL;
-    struct signature_stats *stats_iter = NULL;
-    char *prompt;
-    char *choices;
-
-    fp = es_fopenmem (0, "rw,samethread");
-    if (! fp)
-      log_fatal ("error creating memory stream: %s\n",
-                 gpg_strerror (gpg_error_from_syserror()));
-
-    /* Format the first part of the message.  */
-    {
-      estream_t fp1;
-      char *binding = xasprintf ("<%s, %s>", fingerprint, email);
-      int binding_shown = 0;
-      char *tmpstr, *text;
-
-      fp1 = es_fopenmem (0, "rw,samethread");
-      if (!fp1)
-        log_fatal ("error creating memory stream: %s\n",
-                   gpg_strerror (gpg_error_from_syserror()));
-
-      if (policy == TOFU_POLICY_NONE)
+  if (change_conflicting_to_ask)
+    {
+      if (! may_ask)
         {
-          es_fprintf (fp1, _("The binding %s is NOT known."), binding);
-          es_fputs ("  ", fp1);
-          binding_shown = 1;
+          /* If we weren't allowed to ask, also update this key as
+             conflicting with itself.  */
+          rc = gpgsql_exec_printf
+            (dbs->db, NULL, NULL, &sqerr,
+             "update bindings set policy = %d, conflict = %Q"
+             " where email = %Q"
+             "  and (policy = %d or (policy = %d and fingerprint = %Q));",
+             TOFU_POLICY_ASK, fingerprint, email, TOFU_POLICY_AUTO,
+             TOFU_POLICY_ASK, fingerprint);
         }
-      else if (policy == TOFU_POLICY_ASK
-               /* If there the conflict is with itself, then don't
-                  display this message.  */
-               && conflict && strcmp (conflict, fingerprint) != 0)
+      else
         {
-          es_fprintf (fp1,
-                      _("The key with fingerprint %s raised a conflict "
-                        "with the binding %s."
-                        "  Since this binding's policy was 'auto', it was "
-                        "changed to 'ask'."),
-                      conflict, binding);
-          es_fputs ("  ", fp1);
-          binding_shown = 1;
+          rc = gpgsql_exec_printf
+            (dbs->db, NULL, NULL, &sqerr,
+             "update bindings set policy = %d, conflict = %Q"
+             " where email = %Q and fingerprint != %Q and policy = %d;",
+             TOFU_POLICY_ASK, fingerprint, email, fingerprint,
+             TOFU_POLICY_AUTO);
         }
 
-      /* TRANSLATORS: The %s%s is replaced by either a fingerprint and a
-         blank or by two empty strings.  */
-      es_fprintf (fp1,
-                  _("Please indicate whether you believe the binding %s%s"
-                    "is legitimate (the key belongs to the stated owner) "
-                    "or a forgery (bad)."),
-                  binding_shown ? "" : binding,
-                  binding_shown ? "" : " ");
-      es_fputc ('\n', fp1);
-
-      xfree (binding);
-
-      es_fputc (0, fp1);
-      if (es_fclose_snatch (fp1, (void **)&tmpstr, NULL))
-        log_fatal ("error snatching memory stream\n");
-      text = format_text (tmpstr, 0, 72, 80);
-      es_free (tmpstr);
-
-      es_fputs (text, fp);
-      xfree (text);
-    }
-
-    es_fputc ('\n', fp);
-
-    /* Find other user ids associated with this key and whether the
-       bindings are marked as good or bad.  */
-    {
-      struct db *db_key;
-
-      if (opt.tofu_db_format == TOFU_DB_SPLIT)
-       /* In the split format, we need to search in the fingerprint
-          DB for all the emails associated with this key, not the
-          email DB.  */
-       db_key = getdb (dbs, fingerprint, DB_KEY);
-      else
-       db_key = db;
-
-      if (db_key)
-       {
-         rc = sqlite3_stepx
-           (db_key->db, &db_key->s.get_trust_gather_other_user_ids,
-             strings_collect_cb2, &other_user_ids, &err,
-             opt.tofu_db_format == TOFU_DB_SPLIT
-            ? "select user_id, email from bindings where fingerprint = ?;"
-            : "select user_id, policy from bindings where fingerprint = ?;",
-            SQLITE_ARG_STRING, fingerprint, SQLITE_ARG_END);
-         if (rc)
-           {
-             log_error (_("error gathering other user IDs: %s\n"), err);
-             sqlite3_free (err);
-             err = NULL;
-           }
-       }
-    }
-
-    if (other_user_ids)
-      {
-       strlist_t strlist_iter;
-
-       es_fprintf (fp, _("Known user IDs associated with this key:\n"));
-       for (strlist_iter = other_user_ids;
-            strlist_iter;
-            strlist_iter = strlist_iter->next)
-         {
-           char *other_user_id = strlist_iter->d;
-           char *other_thing;
-           enum tofu_policy other_policy;
-
-           log_assert (strlist_iter->next);
-           strlist_iter = strlist_iter->next;
-           other_thing = strlist_iter->d;
-
-           if (opt.tofu_db_format == TOFU_DB_SPLIT)
-             other_policy = get_policy (dbs, fingerprint, other_thing, NULL);
-           else
-             other_policy = atoi (other_thing);
-
-           es_fprintf (fp, "  %s (", other_user_id);
-           es_fprintf (fp, _("policy: %s"), tofu_policy_str (other_policy));
-           es_fprintf (fp, ")\n");
-          }
-       es_fprintf (fp, "\n");
-
-       free_strlist (other_user_ids);
-      }
-
-    /* Find other keys associated with this email address.  */
-    /* XXX: When generating the statistics, do we want the time
-       embedded in the signature (column 'sig_time') or the time that
-       we first verified the signature (column 'time').  */
-    rc = sqlite3_stepx
-      (db->db, &db->s.get_trust_gather_other_keys,
-       signature_stats_collect_cb, &stats, &err,
-       "select fingerprint, policy, time_ago, count(*)\n"
-       " from (select bindings.*,\n"
-       "        case\n"
-       /* From the future (but if its just a couple of hours in the
-         future don't turn it into a warning)?  Or should we use
-         small, medium or large units?  (Note: whatever we do, we
-         keep the value in seconds.  Then when we group, everything
-         that rounds to the same number of seconds is grouped.)  */
-       "         when delta < -("STRINGIFY (TIME_AGO_FUTURE_IGNORE)") then -1\n"
-       "         when delta < ("STRINGIFY (TIME_AGO_MEDIUM_THRESHOLD)")\n"
-       "          then max(0,\n"
-       "                   round(delta / ("STRINGIFY (TIME_AGO_UNIT_SMALL)"))\n"
-       "               * ("STRINGIFY (TIME_AGO_UNIT_SMALL)"))\n"
-       "         when delta < ("STRINGIFY (TIME_AGO_LARGE_THRESHOLD)")\n"
-       "          then round(delta / ("STRINGIFY (TIME_AGO_UNIT_MEDIUM)"))\n"
-       "               * ("STRINGIFY (TIME_AGO_UNIT_MEDIUM)")\n"
-       "         else round(delta / ("STRINGIFY (TIME_AGO_UNIT_LARGE)"))\n"
-       "              * ("STRINGIFY (TIME_AGO_UNIT_LARGE)")\n"
-       "        end time_ago,\n"
-       "        delta time_ago_raw\n"
-       "       from bindings\n"
-       "       left join\n"
-       "         (select *,\n"
-       "            cast(strftime('%s','now') - sig_time as real) delta\n"
-       "           from signatures) ss\n"
-       "        on ss.binding = bindings.oid)\n"
-       " where email = ?\n"
-       " group by fingerprint, time_ago\n"
-       /* Make sure the current key is first.  */
-       " order by fingerprint = ? asc, fingerprint desc, time_ago desc;\n",
-       SQLITE_ARG_STRING, email, SQLITE_ARG_STRING, fingerprint,
-       SQLITE_ARG_END);
-    if (rc)
-      {
-       strlist_t strlist_iter;
-
-       log_error (_("error gathering signature stats: %s\n"), err);
-       sqlite3_free (err);
-       err = NULL;
-
-       es_fprintf (fp, ngettext("The email address \"%s\" is"
-                                 " associated with %d key:\n",
-                                 "The email address \"%s\" is"
-                                 " associated with %d keys:\n",
-                                 bindings_with_this_email_count),
-                    email, bindings_with_this_email_count);
-       for (strlist_iter = bindings_with_this_email;
-            strlist_iter;
-            strlist_iter = strlist_iter->next)
-         es_fprintf (fp, "  %s\n", strlist_iter->d);
-      }
-    else
-      {
-       char *key = NULL;
-
-       if (! stats || strcmp (stats->fingerprint, fingerprint) != 0)
-         /* If we have already added this key to the DB, then it will
-            be first (see the above select).  Since the first key on
-            the list is not this key, we must not yet have verified
-            any messages signed by this key.  Add a dummy entry.  */
-         signature_stats_prepend (&stats, fingerprint, TOFU_POLICY_AUTO, 0, 0);
-
-       es_fprintf
-          (fp, _("Statistics for keys with the email address \"%s\":\n"),
-           email);
-       for (stats_iter = stats; stats_iter; stats_iter = stats_iter->next)
-         {
-           if (! key || strcmp (key, stats_iter->fingerprint) != 0)
-             {
-               int this_key;
-                char *key_pp;
-               key = stats_iter->fingerprint;
-               this_key = strcmp (key, fingerprint) == 0;
-                key_pp = format_hexfingerprint (key, NULL, 0);
-                es_fprintf (fp, "  %s (", key_pp);
-               if (this_key)
-                 es_fprintf (fp, _("this key"));
-               else
-                 es_fprintf (fp, _("policy: %s"),
-                             tofu_policy_str (stats_iter->policy));
-                es_fputs ("):\n", fp);
-                xfree (key_pp);
-             }
-
-            es_fputs ("    ", fp);
-           if (stats_iter->time_ago == -1)
-             es_fprintf (fp, ngettext("%ld message signed in the future.",
-                                       "%ld messages signed in the future.",
-                                       stats_iter->count), stats_iter->count);
-           else
-              {
-                long t_scaled = time_ago_scale (stats_iter->time_ago);
-
-                /* TANSLATORS: This string is concatenated with one of
-                 * the day/week/month strings to form one sentence.  */
-                es_fprintf (fp, ngettext("%ld message signed",
-                                         "%ld messages signed",
-                                         stats_iter->count), stats_iter->count);
-                if (!stats_iter->count)
-                  es_fputs (".", fp);
-                else if (stats_iter->time_ago < TIME_AGO_UNIT_MEDIUM)
-                  es_fprintf (fp, ngettext(" over the past %ld day.",
-                                           " over the past %ld days.",
-                                           t_scaled), t_scaled);
-                else if (stats_iter->time_ago < TIME_AGO_UNIT_LARGE)
-                  es_fprintf (fp, ngettext(" over the past %ld week.",
-                                           " over the past %ld weeks.",
-                                           t_scaled), t_scaled);
-                else
-                  es_fprintf (fp, ngettext(" over the past %ld month.",
-                                           " over the past %ld months.",
-                                           t_scaled), t_scaled);
-              }
-            es_fputs ("\n", fp);
-         }
-      }
-
-    if (is_conflict)
-      {
-       /* TRANSLATORS: Please translate the text found in the source
-          file below.  We don't directly internationalize that text
-          so that we can tweak it without breaking translations.  */
-       char *text = _("TOFU detected a binding conflict");
-       if (strcmp (text, "TOFU detected a binding conflict") == 0)
-         /* No translation.  Use the English text.  */
-         text =
-           "Normally, there is only a single key associated with an email "
-           "address.  However, people sometimes generate a new key if "
-           "their key is too old or they think it might be compromised.  "
-           "Alternatively, a new key may indicate a man-in-the-middle "
-           "attack!  Before accepting this key, you should talk to or "
-           "call the person to make sure this new key is legitimate.";
-        text = format_text (text, 0, 72, 80);
-       es_fprintf (fp, "\n%s\n", text);
-        xfree (text);
-      }
-
-    es_fputc ('\n', fp);
-
-    /* Add a NUL terminator.  */
-    es_fputc (0, fp);
-    if (es_fclose_snatch (fp, (void **) &prompt, NULL))
-      log_fatal ("error snatching memory stream\n");
-
-    /* I think showing the large message once is sufficient.  If we
-       would move it right before the cpr_get many lines will scroll
-       away and the user might not realize that he merely entered a
-       wrong choise (because he does not see that either).  As a small
-       benefit we allow C-L to redisplay everything.  */
-    tty_printf ("%s", prompt);
-    while (1)
-      {
-       char *response;
-
-        /* TRANSLATORS: Two letters (normally the lower and upper case
-           version of the hotkey) for each of the five choices.  If
-           there is only one choice in your language, repeat it.  */
-        choices = _("gG" "aA" "uU" "rR" "bB");
-       if (strlen (choices) != 10)
-         log_bug ("Bad TOFU conflict translation!  Please report.");
-
-       response = cpr_get
-          ("tofu.conflict",
-           _("(G)ood, (A)ccept once, (U)nknown, (R)eject once, (B)ad? "));
-       trim_spaces (response);
-       cpr_kill_prompt ();
-        if (*response == CONTROL_L)
-          tty_printf ("%s", prompt);
-       else if (strlen (response) == 1)
-         {
-           char *choice = strchr (choices, *response);
-           if (choice)
-             {
-               int c = ((size_t) choice - (size_t) choices) / 2;
-
-               switch (c)
-                 {
-                 case 0: /* Good.  */
-                   policy = TOFU_POLICY_GOOD;
-                   trust_level = tofu_policy_to_trust_level (policy);
-                   break;
-                 case 1: /* Accept once.  */
-                   policy = TOFU_POLICY_ASK;
-                   trust_level =
-                     tofu_policy_to_trust_level (TOFU_POLICY_GOOD);
-                   break;
-                 case 2: /* Unknown.  */
-                   policy = TOFU_POLICY_UNKNOWN;
-                   trust_level = tofu_policy_to_trust_level (policy);
-                   break;
-                 case 3: /* Reject once.  */
-                   policy = TOFU_POLICY_ASK;
-                   trust_level =
-                     tofu_policy_to_trust_level (TOFU_POLICY_BAD);
-                   break;
-                 case 4: /* Bad.  */
-                   policy = TOFU_POLICY_BAD;
-                   trust_level = tofu_policy_to_trust_level (policy);
-                   break;
-                 default:
-                   log_bug ("c should be between 0 and 4 but it is %d!", c);
-                 }
-
-               if (record_binding (dbs, fingerprint, email, user_id,
-                                   policy, 0) != 0)
-                 /* If there's an error registering the
-                    binding, don't save the signature.  */
-                 trust_level = _tofu_GET_TRUST_ERROR;
-
-               break;
-             }
-         }
-       xfree (response);
-      }
-
-    xfree (prompt);
-
-    signature_stats_free (stats);
-  }
-
- out:
-  if (change_conflicting_to_ask)
-    {
-      if (! may_ask)
-       /* If we weren't allowed to ask, also update this key as
-          conflicting with itself.  */
-       rc = sqlite3_exec_printf
-         (db->db, NULL, NULL, &err,
-          "update bindings set policy = %d, conflict = %Q"
-          " where email = %Q"
-          "  and (policy = %d or (policy = %d and fingerprint = %Q));",
-          TOFU_POLICY_ASK, fingerprint, email, TOFU_POLICY_AUTO,
-          TOFU_POLICY_ASK, fingerprint);
-      else
-       rc = sqlite3_exec_printf
-         (db->db, NULL, NULL, &err,
-          "update bindings set policy = %d, conflict = %Q"
-          " where email = %Q and fingerprint != %Q and policy = %d;",
-          TOFU_POLICY_ASK, fingerprint, email, fingerprint, TOFU_POLICY_AUTO);
       if (rc)
        {
-         log_error (_("error changing TOFU policy: %s\n"), err);
-         sqlite3_free (err);
-         goto out;
+         log_error (_("error changing TOFU policy: %s\n"), sqerr);
+         sqlite3_free (sqerr);
+          sqerr = NULL;
        }
     }
 
   xfree (conflict);
   free_strlist (bindings_with_this_email);
-  xfree (fingerprint_pp);
 
   return trust_level;
 }
@@ -2365,27 +2055,71 @@ time_ago_str (long long int t)
 }
 
 
+/* If FP is NULL, write TOFU_STATS status line.  If FP is not NULL
+ * write a "tfs" record to that stream. */
+static void
+write_stats_status (estream_t fp, long messages, enum tofu_policy policy,
+                    unsigned long first_seen,
+                    unsigned long most_recent_seen)
+{
+  const char *validity;
+
+  if (messages < 1)
+    validity = "1"; /* Key without history.  */
+  else if (messages < BASIC_TRUST_THRESHOLD)
+    validity = "2"; /* Key with too little history.  */
+  else if (messages < FULL_TRUST_THRESHOLD)
+    validity = "3"; /* Key with enough history for basic trust.  */
+  else
+    validity = "4"; /* Key with a lot of history.  */
+
+  if (fp)
+    {
+      es_fprintf (fp, "tfs:1:%s:%ld:0:%s:%lu:%lu:\n",
+                  validity, messages,
+                  tofu_policy_str (policy),
+                  first_seen, most_recent_seen);
+    }
+  else
+    {
+      char numbuf1[35];
+      char numbuf2[35];
+      char numbuf3[35];
+
+      snprintf (numbuf1, sizeof numbuf1, " %ld", messages);
+      *numbuf2 = *numbuf3 = 0;
+      if (first_seen && most_recent_seen)
+        {
+          snprintf (numbuf2, sizeof numbuf2, " %lu", first_seen);
+          snprintf (numbuf3, sizeof numbuf3, " %lu", most_recent_seen);
+        }
+
+      write_status_strings (STATUS_TOFU_STATS,
+                            validity, numbuf1, " 0",
+                            " ", tofu_policy_str (policy),
+                            numbuf2, numbuf3,
+                            NULL);
+    }
+}
+
+
+/* Note: If OUTFP is not NULL, this function merely prints a "tfs" record
+ * to OUTFP.  In this case USER_ID is not required.  */
 static void
-show_statistics (struct dbs *dbs, const char *fingerprint,
+show_statistics (tofu_dbs_t dbs, const char *fingerprint,
                 const char *email, const char *user_id,
-                const char *sig_exclude)
+                const char *sig_exclude, estream_t outfp)
 {
-  struct db *db;
   char *fingerprint_pp;
   int rc;
   strlist_t strlist = NULL;
   char *err = NULL;
 
-  db = getdb (dbs, email, DB_EMAIL);
-  if (! db)
-    return;
-
   fingerprint_pp = format_hexfingerprint (fingerprint, NULL, 0);
 
-  rc = sqlite3_exec_printf
-    (db->db, strings_collect_cb, &strlist, &err,
-     "select count (*), strftime('%%s','now') - min (signatures.time),\n"
-     "  strftime('%%s','now') - max (signatures.time)\n"
+  rc = gpgsql_exec_printf
+    (dbs->db, strings_collect_cb, &strlist, &err,
+     "select count (*), min (signatures.time), max (signatures.time)\n"
      " from signatures\n"
      " left join bindings on signatures.binding = bindings.oid\n"
      " where fingerprint = %Q and email = %Q and sig_digest %s%s%s;",
@@ -2403,17 +2137,23 @@ show_statistics (struct dbs *dbs, const char *fingerprint,
       goto out;
     }
 
-  write_status_text_and_buffer (STATUS_TOFU_USER, fingerprint,
-                                email, strlen (email), 0);
+  if (!outfp)
+    write_status_text_and_buffer (STATUS_TOFU_USER, fingerprint,
+                                  email, strlen (email), 0);
 
   if (! strlist)
-    log_info (_("Have never verified a message signed by key %s!\n"),
-              fingerprint_pp);
+    {
+      if (!outfp)
+        log_info (_("Have never verified a message signed by key %s!\n"),
+                  fingerprint_pp);
+      write_stats_status (outfp, 0, TOFU_POLICY_NONE, 0, 0);
+    }
   else
     {
+      unsigned long now = gnupg_get_time ();
       signed long messages;
-      signed long first_seen_ago;
-      signed long most_recent_seen_ago;
+      unsigned long first_seen;
+      unsigned long most_recent_seen;
 
       log_assert (strlist_length (strlist) == 3);
 
@@ -2421,26 +2161,53 @@ show_statistics (struct dbs *dbs, const char *fingerprint,
 
       if (messages == 0 && *strlist->next->d == '\0')
         { /* min(NULL) => NULL => "".  */
-          first_seen_ago = -1;
-          most_recent_seen_ago = -1;
+          first_seen = 0;
+          most_recent_seen = 0;
         }
       else
        {
-          string_to_long (&first_seen_ago, strlist->next->d, 0, __LINE__);
-         string_to_long (&most_recent_seen_ago, strlist->next->next->d, 0,
-                          __LINE__);
+          string_to_ulong (&first_seen, strlist->next->d, -1, __LINE__);
+          if (first_seen > now)
+            {
+              log_debug ("time-warp - tofu DB has a future value (%lu, %lu)\n",
+                         first_seen, now);
+              first_seen = now;
+            }
+         string_to_ulong (&most_recent_seen, strlist->next->next->d, -1,
+                           __LINE__);
+          if (most_recent_seen > now)
+            {
+              log_debug ("time-warp - tofu DB has a future value (%lu, %lu)\n",
+                         most_recent_seen, now);
+              most_recent_seen = now;
+            }
+
        }
 
-      if (messages == -1 || first_seen_ago == 0)
-        log_info (_("Failed to collect signature statistics for \"%s\"\n"
-                    "(key %s)\n"),
-                  user_id, fingerprint_pp);
+      if (messages == -1 || first_seen == -1)
+        {
+          write_stats_status (outfp, 0, TOFU_POLICY_NONE, 0, 0);
+          if (!outfp)
+            log_info (_("Failed to collect signature statistics for \"%s\"\n"
+                        "(key %s)\n"),
+                      user_id, fingerprint_pp);
+        }
+      else if (outfp)
+        {
+          write_stats_status (outfp, messages,
+                              get_policy (dbs, fingerprint, email, NULL),
+                              first_seen, most_recent_seen);
+        }
       else
        {
          enum tofu_policy policy = get_policy (dbs, fingerprint, email, NULL);
          estream_t fp;
          char *msg;
 
+          write_stats_status (NULL, messages,
+                              policy,
+                              first_seen, most_recent_seen);
+
          fp = es_fopenmem (0, "rw,samethread");
          if (! fp)
             log_fatal ("error creating memory stream: %s\n",
@@ -2454,7 +2221,7 @@ show_statistics (struct dbs *dbs, const char *fingerprint,
             }
          else
            {
-              char *first_seen_ago_str = time_ago_str (first_seen_ago);
+              char *first_seen_ago_str = time_ago_str (now - first_seen);
 
               /* TRANSLATORS: The final %s is replaced by a string like
                  "7 months, 1 day, 5 minutes, 0 seconds". */
@@ -2468,7 +2235,7 @@ show_statistics (struct dbs *dbs, const char *fingerprint,
 
               if (messages > 1)
                 {
-                  char *tmpstr = time_ago_str (most_recent_seen_ago);
+                  char *tmpstr = time_ago_str (now - most_recent_seen);
                   es_fputs ("  ", fp);
                   es_fprintf (fp, _("The most recent message was"
                                     " verified %s ago."), tmpstr);
@@ -2494,15 +2261,24 @@ show_statistics (struct dbs *dbs, const char *fingerprint,
               log_fatal ("error snatching memory stream\n");
             msg = format_text (tmpmsg, 0, 72, 80);
             es_free (tmpmsg);
+
+            /* Print a status line but suppress the trailing LF.
+             * Spaces are not percent escaped. */
+            if (*msg)
+              write_status_buffer (STATUS_TOFU_STATS_LONG,
+                                   msg, strlen (msg)-1, -1);
+
+            /* Remove the non-breaking space markers.  */
             for (p=msg; *p; p++)
               if (*p == '~')
                 *p = ' ';
+
           }
 
          log_string (GPGRT_LOG_INFO, msg);
           xfree (msg);
 
-         if (policy == TOFU_POLICY_AUTO && messages < NO_WARNING_THRESHOLD)
+         if (policy == TOFU_POLICY_AUTO && messages < BASIC_TRUST_THRESHOLD)
            {
              char *set_policy_command;
              char *text;
@@ -2569,8 +2345,9 @@ email_from_user_id (const char *user_id)
   return email;
 }
 
-/* Register the signature with the binding <fingerprint, USER_ID>.
-   The fingerprint is taken from the primary key packet PK.
+/* Register the signature with the bindings <fingerprint, USER_ID>,
+   for each USER_ID in USER_ID_LIST.  The fingerprint is taken from
+   the primary key packet PK.
 
    SIG_DIGEST_BIN is the binary representation of the message's
    digest.  SIG_DIGEST_BIN_LEN is its length.
@@ -2586,174 +2363,152 @@ email_from_user_id (const char *user_id)
    This is necessary if there is a conflict or the binding's policy is
    TOFU_POLICY_ASK.
 
-   This function returns the binding's trust level on return.  If an
-   error occurs, this function returns TRUST_UNKNOWN.  */
-int
-tofu_register (PKT_public_key *pk, const char *user_id,
+   This function returns 0 on success and an error code if an error
+   occured.  */
+gpg_error_t
+tofu_register (ctrl_t ctrl, PKT_public_key *pk, strlist_t user_id_list,
               const byte *sig_digest_bin, int sig_digest_bin_len,
-              time_t sig_time, const char *origin, int may_ask)
+              time_t sig_time, const char *origin)
 {
-  struct dbs *dbs;
-  struct db *db;
+  gpg_error_t rc;
+  tofu_dbs_t dbs;
   char *fingerprint = NULL;
-  char *fingerprint_pp = NULL;
+  strlist_t user_id;
   char *email = NULL;
   char *err = NULL;
-  int rc;
-  int trust_level = TRUST_UNKNOWN;
   char *sig_digest;
   unsigned long c;
-  int already_verified = 0;
-
-  sig_digest = make_radix64_string (sig_digest_bin, sig_digest_bin_len);
 
-  dbs = opendbs ();
+  dbs = opendbs (ctrl);
   if (! dbs)
     {
+      rc = gpg_error (GPG_ERR_GENERAL);
       log_error (_("error opening TOFU database: %s\n"),
-                 gpg_strerror (GPG_ERR_GENERAL));
-      goto die;
+                 gpg_strerror (rc));
+      return rc;
     }
 
-  fingerprint = hexfingerprint (pk, NULL, 0);
-  fingerprint_pp = format_hexfingerprint (fingerprint, NULL, 0);
-
-  if (! *user_id)
-    {
-      log_debug ("TOFU: user id is empty.  Can't continue.\n");
-      goto die;
-    }
+  /* We do a query and then an insert.  Make sure they are atomic
+     by wrapping them in a transaction.  */
+  rc = begin_transaction (ctrl, 0);
+  if (rc)
+    return rc;
 
-  email = email_from_user_id (user_id);
+  sig_digest = make_radix64_string (sig_digest_bin, sig_digest_bin_len);
+  fingerprint = hexfingerprint (pk, NULL, 0);
 
   if (! origin)
     /* The default origin is simply "unknown".  */
     origin = "unknown";
 
-  /* It's necessary to get the trust so that we are certain that the
-     binding has been registered.  */
-  trust_level = get_trust (dbs, fingerprint, email, user_id, may_ask);
-  if (trust_level == _tofu_GET_TRUST_ERROR)
-    /* An error.  */
-    {
-      trust_level = TRUST_UNKNOWN;
-      goto die;
-    }
-
-  /* Save the observed signature in the DB.  */
-  db = getdb (dbs, email, DB_EMAIL);
-  if (! db)
+  for (user_id = user_id_list; user_id; user_id = user_id->next)
     {
-      log_error (_("error opening TOFU database: %s\n"),
-                 gpg_strerror (GPG_ERR_GENERAL));
-      goto die;
-    }
+      email = email_from_user_id (user_id->d);
 
-  /* We do a query and then an insert.  Make sure they are atomic
-     by wrapping them in a transaction.  */
-  rc = begin_transaction (db, 0);
-  if (rc)
-    goto die;
-
-  /* If we've already seen this signature before, then don't add
-     it again.  */
-  rc = sqlite3_stepx
-    (db->db, &db->s.register_already_seen,
-     get_single_unsigned_long_cb2, &c, &err,
-     "select count (*)\n"
-     " from signatures left join bindings\n"
-     "  on signatures.binding = bindings.oid\n"
-     " where fingerprint = ? and email = ? and sig_time = ?\n"
-     "  and sig_digest = ?",
-     SQLITE_ARG_STRING, fingerprint, SQLITE_ARG_STRING, email,
-     SQLITE_ARG_LONG_LONG, (long long) sig_time,
-     SQLITE_ARG_STRING, sig_digest,
-     SQLITE_ARG_END);
-  if (rc)
-    {
-      log_error (_("error reading TOFU database: %s\n"), err);
-      print_further_info ("checking existence");
-      sqlite3_free (err);
-    }
-  else if (c > 1)
-    /* Duplicates!  This should not happen.  In particular,
-       because <fingerprint, email, sig_time, sig_digest> is the
-       primary key!  */
-    log_debug ("SIGNATURES DB contains duplicate records"
-              " <%s, %s, 0x%lx, %s, %s>."
-              "  Please report.\n",
-              fingerprint, email, (unsigned long) sig_time,
-              sig_digest, origin);
-  else if (c == 1)
-    {
-      already_verified = 1;
       if (DBG_TRUST)
-       log_debug ("Already observed the signature"
-                  " <%s, %s, 0x%lx, %s, %s>\n",
-                  fingerprint, email, (unsigned long) sig_time,
-                  sig_digest, origin);
-    }
-  else if (opt.dry_run)
-    {
-      log_info ("TOFU database update skipped due to --dry-run\n");
-    }
-  else
-    /* This is the first time that we've seen this signature.
-       Record it.  */
-    {
-      if (DBG_TRUST)
-       log_debug ("TOFU: Saving signature <%s, %s, %s>\n",
-                  fingerprint, email, sig_digest);
-
-      log_assert (c == 0);
-
-      rc = sqlite3_stepx
-       (db->db, &db->s.register_insert, NULL, NULL, &err,
-        "insert into signatures\n"
-        " (binding, sig_digest, origin, sig_time, time)\n"
-        " values\n"
-        " ((select oid from bindings\n"
-        "    where fingerprint = ? and email = ?),\n"
-        "  ?, ?, ?, strftime('%s', 'now'));",
-        SQLITE_ARG_STRING, fingerprint, SQLITE_ARG_STRING, email,
-         SQLITE_ARG_STRING, sig_digest, SQLITE_ARG_STRING, origin,
-         SQLITE_ARG_LONG_LONG, (long long) sig_time,
-         SQLITE_ARG_END);
+       log_debug ("TOFU: Registering signature %s with binding"
+                   " <key: %s, user id: %s>\n",
+                  sig_digest, fingerprint, email);
+
+      /* Make sure the binding exists and record any TOFU
+         conflicts.  */
+      if (get_trust (ctrl, pk, fingerprint, email, user_id->d, 0)
+          == _tofu_GET_TRUST_ERROR)
+        {
+          rc = gpg_error (GPG_ERR_GENERAL);
+          xfree (email);
+          break;
+        }
+
+      /* If we've already seen this signature before, then don't add
+         it again.  */
+      rc = gpgsql_stepx
+        (dbs->db, &dbs->s.register_already_seen,
+         get_single_unsigned_long_cb2, &c, &err,
+         "select count (*)\n"
+         " from signatures left join bindings\n"
+         "  on signatures.binding = bindings.oid\n"
+         " where fingerprint = ? and email = ? and sig_time = ?\n"
+         "  and sig_digest = ?",
+         GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_STRING, email,
+         GPGSQL_ARG_LONG_LONG, (long long) sig_time,
+         GPGSQL_ARG_STRING, sig_digest,
+         GPGSQL_ARG_END);
       if (rc)
-       {
-         log_error (_("error updating TOFU database: %s\n"), err);
-          print_further_info ("insert signatures");
-         sqlite3_free (err);
-       }
+        {
+          log_error (_("error reading TOFU database: %s\n"), err);
+          print_further_info ("checking existence");
+          sqlite3_free (err);
+        }
+      else if (c > 1)
+        /* Duplicates!  This should not happen.  In particular,
+           because <fingerprint, email, sig_time, sig_digest> is the
+           primary key!  */
+        log_debug ("SIGNATURES DB contains duplicate records"
+                   " <key: %s, fingerprint: %s, time: 0x%lx, sig: %s,"
+                   " origin: %s>."
+                   "  Please report.\n",
+                   fingerprint, email, (unsigned long) sig_time,
+                   sig_digest, origin);
+      else if (c == 1)
+        {
+          if (DBG_TRUST)
+            log_debug ("Already observed the signature and binding"
+                       " <key: %s, user id: %s, time: 0x%lx, sig: %s,"
+                       " origin: %s>\n",
+                       fingerprint, email, (unsigned long) sig_time,
+                       sig_digest, origin);
+        }
+      else if (opt.dry_run)
+        {
+          log_info ("TOFU database update skipped due to --dry-run\n");
+        }
+      else
+        /* This is the first time that we've seen this signature and
+           binding.  Record it.  */
+        {
+          if (DBG_TRUST)
+            log_debug ("TOFU: Saving signature"
+                       " <key: %s, user id: %s, sig: %s>\n",
+                       fingerprint, email, sig_digest);
+
+          log_assert (c == 0);
+
+          rc = gpgsql_stepx
+            (dbs->db, &dbs->s.register_insert, NULL, NULL, &err,
+             "insert into signatures\n"
+             " (binding, sig_digest, origin, sig_time, time)\n"
+             " values\n"
+             " ((select oid from bindings\n"
+             "    where fingerprint = ? and email = ?),\n"
+             "  ?, ?, ?, strftime('%s', 'now'));",
+             GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_STRING, email,
+             GPGSQL_ARG_STRING, sig_digest, GPGSQL_ARG_STRING, origin,
+             GPGSQL_ARG_LONG_LONG, (long long) sig_time,
+             GPGSQL_ARG_END);
+          if (rc)
+            {
+              log_error (_("error updating TOFU database: %s\n"), err);
+              print_further_info ("insert signatures");
+              sqlite3_free (err);
+            }
+        }
+
+      xfree (email);
+
+      if (rc)
+        break;
     }
 
-  /* It only matters whether we abort or commit the transaction
-     (so long as we do something) if we execute the insert.  */
   if (rc)
-    rc = rollback_transaction (db);
+    rollback_transaction (ctrl);
   else
-    rc = end_transaction (db, 0);
-  if (rc)
-    {
-      sqlite3_free (err);
-      goto die;
-    }
-
- die:
-  if (may_ask && trust_level != TRUST_ULTIMATE)
-    /* It's only appropriate to show the statistics in an interactive
-       context.  */
-    show_statistics (dbs, fingerprint, email, user_id,
-                    already_verified ? NULL : sig_digest);
+    rc = end_transaction (ctrl, 0);
 
-  xfree (email);
-  xfree (fingerprint_pp);
   xfree (fingerprint);
-  if (dbs)
-    closedbs (dbs);
   xfree (sig_digest);
 
-  return trust_level;
+  return rc;
 }
 
 /* Combine a trust level returned from the TOFU trust model with a
@@ -2819,57 +2574,140 @@ tofu_wot_trust_combine (int tofu_base, int wot_base)
 }
 
 
-/* Return the validity (TRUST_NEVER, etc.) of the binding
-   <FINGERPRINT, USER_ID>.
+/* Write a "tfs" record for a --with-colons listing.  */
+gpg_error_t
+tofu_write_tfs_record (ctrl_t ctrl, estream_t fp,
+                       PKT_public_key *pk, const char *user_id)
+{
+  gpg_error_t err;
+  tofu_dbs_t dbs;
+  char *fingerprint;
+  char *email;
+
+  if (!*user_id)
+    return 0;  /* No TOFU stats possible for an empty ID.  */
+
+  dbs = opendbs (ctrl);
+  if (!dbs)
+    {
+      err = gpg_error (GPG_ERR_GENERAL);
+      log_error (_("error opening TOFU database: %s\n"), gpg_strerror (err));
+      return err;
+    }
+
+  fingerprint = hexfingerprint (pk, NULL, 0);
+  email = email_from_user_id (user_id);
+
+  show_statistics (dbs, fingerprint, email, user_id, NULL, fp);
+
+  xfree (email);
+  xfree (fingerprint);
+  return 0;
+}
+
+
+/* Return the validity (TRUST_NEVER, etc.) of the bindings
+   <FINGERPRINT, USER_ID>, for each USER_ID in USER_ID_LIST.  If
+   USER_ID_LIST->FLAG is set, then the id is considered to be expired.
 
    PK is the primary key packet.
 
    If MAY_ASK is 1 and the policy is TOFU_POLICY_ASK, then the user
-   will be prompted to choose a different policy.  If MAY_ASK is 0 and
-   the policy is TOFU_POLICY_ASK, then TRUST_UNKNOWN is returned.
+   will be prompted to choose a policy.  If MAY_ASK is 0 and the
+   policy is TOFU_POLICY_ASK, then TRUST_UNKNOWN is returned.
 
    Returns TRUST_UNDEFINED if an error occurs.  */
 int
-tofu_get_validity (PKT_public_key *pk, const char *user_id,
+tofu_get_validity (ctrl_t ctrl, PKT_public_key *pk, strlist_t user_id_list,
                   int may_ask)
 {
-  struct dbs *dbs;
+  tofu_dbs_t dbs;
   char *fingerprint = NULL;
-  char *email = NULL;
-  int trust_level = TRUST_UNDEFINED;
+  strlist_t user_id;
+  int trust_level = TRUST_UNKNOWN;
+  int bindings = 0;
+  int bindings_valid = 0;
 
-  dbs = opendbs ();
+  dbs = opendbs (ctrl);
   if (! dbs)
     {
       log_error (_("error opening TOFU database: %s\n"),
                  gpg_strerror (GPG_ERR_GENERAL));
-      goto die;
+      return TRUST_UNDEFINED;
     }
 
   fingerprint = hexfingerprint (pk, NULL, 0);
 
-  if (! *user_id)
+  tofu_begin_batch_update (ctrl);
+  tofu_resume_batch_transaction (ctrl);
+
+  for (user_id = user_id_list; user_id; user_id = user_id->next, bindings ++)
     {
-      log_debug ("user id is empty."
-                 "  Can't get TOFU validity for this binding.\n");
-      goto die;
-    }
+      char *email = email_from_user_id (user_id->d);
 
-  email = email_from_user_id (user_id);
+      /* Always call get_trust to make sure the binding is
+         registered.  */
+      int tl = get_trust (ctrl, pk, fingerprint, email, user_id->d, may_ask);
+      if (tl == _tofu_GET_TRUST_ERROR)
+        {
+          /* An error.  */
+          trust_level = TRUST_UNDEFINED;
+          xfree (email);
+          goto die;
+        }
+
+      if (DBG_TRUST)
+       log_debug ("TOFU: validity for <key: %s, user id: %s>: %s%s.\n",
+                  fingerprint, email,
+                   trust_value_to_string (tl),
+                   user_id->flags ? " (but expired)" : "");
+
+      if (user_id->flags)
+        tl = TRUST_EXPIRED;
+
+      if (tl != TRUST_EXPIRED)
+        bindings_valid ++;
 
-  trust_level = get_trust (dbs, fingerprint, email, user_id, may_ask);
-  if (trust_level == _tofu_GET_TRUST_ERROR)
-    /* An error.  */
-    trust_level = TRUST_UNDEFINED;
+      if (may_ask && tl != TRUST_ULTIMATE && tl != TRUST_EXPIRED)
+        show_statistics (dbs, fingerprint, email, user_id->d, NULL, NULL);
 
-  if (may_ask && trust_level != TRUST_ULTIMATE)
-    show_statistics (dbs, fingerprint, email, user_id, NULL);
+      if (tl == TRUST_NEVER)
+        trust_level = TRUST_NEVER;
+      else if (tl == TRUST_EXPIRED)
+        /* Ignore expired bindings in the trust calculation.  */
+        ;
+      else if (tl > trust_level)
+        {
+          /* The expected values: */
+          log_assert (tl == TRUST_UNKNOWN || tl == TRUST_UNDEFINED
+                      || tl == TRUST_MARGINAL || tl == TRUST_FULLY
+                      || tl == TRUST_ULTIMATE);
+
+          /* We assume the following ordering:  */
+          log_assert (TRUST_UNKNOWN < TRUST_UNDEFINED);
+          log_assert (TRUST_UNDEFINED < TRUST_MARGINAL);
+          log_assert (TRUST_MARGINAL < TRUST_FULLY);
+          log_assert (TRUST_FULLY < TRUST_ULTIMATE);
+
+          trust_level = tl;
+        }
+
+      xfree (email);
+    }
 
  die:
-  xfree (email);
+  tofu_end_batch_update (ctrl);
+
   xfree (fingerprint);
-  if (dbs)
-    closedbs (dbs);
+
+  if (bindings_valid == 0)
+    {
+      if (DBG_TRUST)
+        log_debug ("no (of %d) valid bindings."
+                   "  Can't get TOFU validity for this set of user ids.\n",
+                   bindings);
+      return TRUST_NEVER;
+    }
 
   return trust_level;
 }
@@ -2882,16 +2720,16 @@ tofu_get_validity (PKT_public_key *pk, const char *user_id,
 
    Returns 0 on success and an error code otherwise.  */
 gpg_error_t
-tofu_set_policy (kbnode_t kb, enum tofu_policy policy)
+tofu_set_policy (ctrl_t ctrl, kbnode_t kb, enum tofu_policy policy)
 {
-  struct dbs *dbs;
+  tofu_dbs_t dbs;
   PKT_public_key *pk;
   char *fingerprint = NULL;
 
   log_assert (kb->pkt->pkttype == PKT_PUBLIC_KEY);
   pk = kb->pkt->pkt.public_key;
 
-  dbs = opendbs ();
+  dbs = opendbs (ctrl);
   if (! dbs)
     {
       log_error (_("error opening TOFU database: %s\n"),
@@ -2908,6 +2746,8 @@ tofu_set_policy (kbnode_t kb, enum tofu_policy policy)
 
   fingerprint = hexfingerprint (pk, NULL, 0);
 
+  begin_transaction (ctrl, 0);
+
   for (; kb; kb = kb->next)
     {
       PKT_user_id *user_id;
@@ -2929,9 +2769,9 @@ tofu_set_policy (kbnode_t kb, enum tofu_policy policy)
       xfree (email);
     }
 
-  xfree (fingerprint);
-  closedbs (dbs);
+  end_transaction (ctrl, 0);
 
+  xfree (fingerprint);
   return 0;
 }
 
@@ -2943,13 +2783,13 @@ tofu_set_policy (kbnode_t kb, enum tofu_policy policy)
 
    Returns 0 on success and an error code otherwise.  */
 gpg_error_t
-tofu_set_policy_by_keyid (u32 *keyid, enum tofu_policy policy)
+tofu_set_policy_by_keyid (ctrl_t ctrl, u32 *keyid, enum tofu_policy policy)
 {
   kbnode_t keyblock = get_pubkeyblock (keyid);
   if (! keyblock)
     return gpg_error (GPG_ERR_NO_PUBKEY);
 
-  return tofu_set_policy (keyblock, policy);
+  return tofu_set_policy (ctrl, keyblock, policy);
 }
 
 /* Return the TOFU policy for the specified binding in *POLICY.  If no
@@ -2960,10 +2800,10 @@ tofu_set_policy_by_keyid (u32 *keyid, enum tofu_policy policy)
 
    Returns 0 on success and an error code otherwise.  */
 gpg_error_t
-tofu_get_policy (PKT_public_key *pk, PKT_user_id *user_id,
+tofu_get_policy (ctrl_t ctrl, PKT_public_key *pk, PKT_user_id *user_id,
                 enum tofu_policy *policy)
 {
-  struct dbs *dbs;
+  tofu_dbs_t dbs;
   char *fingerprint;
   char *email;
 
@@ -2971,7 +2811,7 @@ tofu_get_policy (PKT_public_key *pk, PKT_user_id *user_id,
   log_assert (pk->main_keyid[0] == pk->keyid[0]
               && pk->main_keyid[1] == pk->keyid[1]);
 
-  dbs = opendbs ();
+  dbs = opendbs (ctrl);
   if (! dbs)
     {
       log_error (_("error opening TOFU database: %s\n"),
@@ -2987,8 +2827,6 @@ tofu_get_policy (PKT_public_key *pk, PKT_user_id *user_id,
 
   xfree (email);
   xfree (fingerprint);
-  closedbs (dbs);
-
   if (*policy == _tofu_GET_POLICY_ERROR)
     return gpg_error (GPG_ERR_GENERAL);
   return 0;