g10: Be careful to not be in a transaction during long operations
[gnupg.git] / g10 / tofu.c
index 9d562c2..de685a6 100644 (file)
@@ -70,9 +70,6 @@ struct tofu_dbs_s
     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;
@@ -84,7 +81,9 @@ struct tofu_dbs_s
     sqlite3_stmt *register_insert;
   } s;
 
-  int batch_update;
+  int in_batch_transaction;
+  int in_transaction;
+  time_t batch_update_started;
 };
 
 
@@ -112,6 +111,7 @@ struct tofu_dbs_s
 
 /* 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
@@ -162,7 +162,9 @@ tofu_policy_to_trust_level (enum tofu_policy policy)
 
 
 \f
-/* Start a transaction on DB.  */
+/* 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 (ctrl_t ctrl, int only_batch)
 {
@@ -172,29 +174,42 @@ begin_transaction (ctrl_t ctrl, int only_batch)
 
   log_assert (dbs);
 
-  if (ctrl->tofu.batch_update_ref
-      && ctrl->tofu.batch_update_started != gnupg_get_time ())
+  /* 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 ())
     {
-      /* 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 (dbs->batch_update)
-        end_transaction (ctrl, 1);
+      /* If we are in a batch update, then batch updates better have
+         been enabled.  */
+      log_assert (ctrl->tofu.batch_updated_wanted);
 
-      ctrl->tofu.batch_update_started = gnupg_get_time ();
+      end_transaction (ctrl, 2);
 
       /* Yield to allow another process a chance to run.  */
       gpgrt_yield ();
     }
 
-  if (ctrl->tofu.batch_update_ref && !dbs->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)
     {
+      /* 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"),
@@ -203,15 +218,19 @@ begin_transaction (ctrl_t ctrl, int only_batch)
           return gpg_error (GPG_ERR_GENERAL);
         }
 
-      dbs->batch_update = 1;
+      dbs->in_batch_transaction = 1;
+      dbs->batch_update_started = gnupg_get_time ();
     }
 
   if (only_batch)
     return 0;
 
-  rc = gpgsql_stepx (dbs->db, &dbs->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"),
@@ -235,37 +254,49 @@ end_transaction (ctrl_t ctrl, int only_batch)
   int rc;
   char *err = NULL;
 
-  if (!dbs)
-    return 0;  /* Shortcut to allow for easier cleanup code.  */
-
-  if ((!ctrl->tofu.batch_update_ref || only_batch == 2) && dbs->batch_update)
+  if (only_batch)
     {
-      /* The batch transaction is still in open, but we left batch
-       * mode.  */
-      dbs->batch_update = 0;
+      if (!dbs)
+        return 0;  /* Shortcut to allow for easier cleanup code.  */
 
-      rc = gpgsql_stepx (dbs->db, &dbs->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);
-          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 = gpgsql_stepx (dbs->db, &dbs->s.savepoint_inner_commit,
-                      NULL, NULL, &err,
-                      "release inner;", SQLITE_ARG_END);
   if (rc)
     {
       log_error (_("error committing transaction on TOFU database: %s\n"),
@@ -285,20 +316,16 @@ rollback_transaction (ctrl_t ctrl)
   int rc;
   char *err = NULL;
 
-  if (!dbs)
-    return 0;  /* Shortcut to allow for easier cleanup code.  */
+  log_assert (dbs);
+  log_assert (dbs->in_transaction > 0);
 
-  if (dbs->batch_update)
-    {
-      /* Just undo the most recent update; don't revert any progress
-         made by the batch transaction.  */
-      rc = sqlite3_exec (dbs->db, "rollback to inner;", NULL, NULL, &err);
-    }
-  else
-    {
-      /* Rollback the whole she-bang.  */
-      rc = sqlite3_exec (dbs->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)
     {
@@ -314,22 +341,36 @@ rollback_transaction (ctrl_t ctrl)
 void
 tofu_begin_batch_update (ctrl_t ctrl)
 {
-  if (!ctrl->tofu.batch_update_ref)
-    ctrl->tofu.batch_update_started = gnupg_get_time ();
-
-  ctrl->tofu.batch_update_ref ++;
+  ctrl->tofu.batch_updated_wanted ++;
 }
 
 void
 tofu_end_batch_update (ctrl_t ctrl)
 {
-  log_assert (ctrl->tofu.batch_update_ref > 0);
-  ctrl->tofu.batch_update_ref --;
+  log_assert (ctrl->tofu.batch_updated_wanted > 0);
+  ctrl->tofu.batch_updated_wanted --;
+  end_transaction (ctrl, 1);
+}
 
-  if (!ctrl->tofu.batch_update_ref)
-    end_transaction (ctrl, 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
@@ -698,10 +739,11 @@ tofu_closedbs (ctrl_t ctrl)
   if (!dbs)
     return;  /* Not initialized.  */
 
-  if (dbs->batch_update)
-    end_transaction (ctrl, 2);
+  log_assert (dbs->in_transaction == 0);
 
-  /* Arghh, that is asurprising use of the struct.  */
+  end_transaction (ctrl, 2);
+
+  /* Arghh, that is a surprising use of the struct.  */
   for (statements = (void *) &dbs->s;
        (void *) statements < (void *) &(&dbs->s)[1];
        statements ++)
@@ -749,10 +791,6 @@ record_binding (tofu_dbs_t dbs, const char *fingerprint, const char *email,
   char *fingerprint_pp = format_hexfingerprint (fingerprint, NULL, 0);
   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
@@ -762,44 +800,50 @@ record_binding (tofu_dbs_t dbs, const char *fingerprint, const char *email,
     log_bug ("%s: Bad value for policy (%d)!\n", __func__, policy);
 
 
-  if (show_old)
+  if (DBG_TRUST || 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.  */
+
+      /* 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;
+
       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)
-    {
-      rc = 0;
-      goto leave; /* Nothing to do.  */
+      if (policy_old == policy)
+        {
+          rc = 0;
+          goto leave; /* Nothing to do.  */
+        }
     }
 
   if (opt.dry_run)
@@ -819,14 +863,14 @@ record_binding (tofu_dbs_t 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 leave;
@@ -893,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];
 };
@@ -916,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;
@@ -1016,9 +1064,9 @@ get_policy (tofu_dbs_t dbs, const char *fingerprint, const char *email,
                       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);
@@ -1071,7 +1119,7 @@ get_policy (tofu_dbs_t 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)
@@ -1168,9 +1216,11 @@ format_conflict_msg_part1 (int policy, const char *conflict,
  *
  *   - 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 (tofu_dbs_t dbs,
+ask_about_binding (ctrl_t ctrl,
                    enum tofu_policy *policy,
                    int *trust_level,
                    int bindings_with_this_email_count,
@@ -1180,6 +1230,7 @@ ask_about_binding (tofu_dbs_t dbs,
                    const char *email,
                    const char *user_id)
 {
+  tofu_dbs_t dbs;
   char *sqerr = NULL;
   int rc;
   estream_t fp;
@@ -1189,6 +1240,10 @@ ask_about_binding (tofu_dbs_t dbs,
   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",
@@ -1202,13 +1257,15 @@ ask_about_binding (tofu_dbs_t dbs,
     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 = ?;",
-     SQLITE_ARG_STRING, fingerprint, SQLITE_ARG_END);
+     GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_END);
   if (rc)
     {
       log_error (_("error gathering other user IDs: %s\n"), sqerr);
@@ -1281,8 +1338,8 @@ ask_about_binding (tofu_dbs_t dbs,
      " 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);
+     GPGSQL_ARG_STRING, email, GPGSQL_ARG_STRING, fingerprint,
+     GPGSQL_ARG_END);
   if (rc)
     {
       strlist_t strlist_iter;
@@ -1304,6 +1361,7 @@ ask_about_binding (tofu_dbs_t dbs,
     }
   else
     {
+      KEYDB_HANDLE hd;
       char *key = NULL;
 
       if (! stats || strcmp (stats->fingerprint, fingerprint))
@@ -1315,6 +1373,93 @@ ask_about_binding (tofu_dbs_t dbs,
           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)
@@ -1328,6 +1473,18 @@ ask_about_binding (tofu_dbs_t dbs,
               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
@@ -1370,9 +1527,11 @@ ask_about_binding (tofu_dbs_t dbs,
         }
     }
 
+  end_transaction (ctrl, 0);
 
   if ((*policy == TOFU_POLICY_NONE && bindings_with_this_email_count > 0)
-      || (*policy == TOFU_POLICY_ASK && conflict))
+      || (*policy == TOFU_POLICY_ASK
+          && (conflict || bindings_with_this_email_count > 0)))
     {
       /* This is a conflict.  */
 
@@ -1393,7 +1552,7 @@ ask_about_binding (tofu_dbs_t dbs,
             "call the person to make sure this new key is legitimate.";
         }
       textbuf = format_text (text, 0, 72, 80);
-      es_fprintf (fp, "\n%s\n", text);
+      es_fprintf (fp, "\n%s\n", textbuf);
       xfree (textbuf);
     }
 
@@ -1410,6 +1569,10 @@ ask_about_binding (tofu_dbs_t dbs,
    * 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;
@@ -1473,6 +1636,7 @@ ask_about_binding (tofu_dbs_t dbs,
         }
       xfree (response);
     }
+  tofu_resume_batch_transaction (ctrl);
 
   xfree (prompt);
 
@@ -1493,12 +1657,15 @@ ask_about_binding (tofu_dbs_t dbs,
  * 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.  */
+ * 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 (tofu_dbs_t dbs, PKT_public_key *pk,
+get_trust (ctrl_t ctrl, PKT_public_key *pk,
            const char *fingerprint, const char *email,
           const char *user_id, int may_ask)
 {
+  tofu_dbs_t dbs = ctrl->tofu.dbs;
+  int in_transaction = 0;
   enum tofu_policy policy;
   char *conflict = NULL;
   int rc;
@@ -1508,6 +1675,11 @@ get_trust (tofu_dbs_t dbs, PKT_public_key *pk,
   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;
 
@@ -1521,37 +1693,41 @@ get_trust (tofu_dbs_t dbs, PKT_public_key *pk,
               && _tofu_GET_TRUST_ERROR != TRUST_FULLY
               && _tofu_GET_TRUST_ERROR != TRUST_ULTIMATE);
 
+  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.  */
-      u32 kid[2];
+  {
+    /* See if the key is ultimately trusted.  If so, we're done.  */
+    u32 kid[2];
 
-      keyid_from_pk (pk, kid);
+    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));
     }
@@ -1564,7 +1740,7 @@ get_trust (tofu_dbs_t dbs, PKT_public_key *pk,
       /* The saved judgement is auto -> auto, good, unknown or bad.
        * 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;
@@ -1618,7 +1794,7 @@ get_trust (tofu_dbs_t dbs, PKT_public_key *pk,
     (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"), sqerr);
@@ -1644,8 +1820,8 @@ get_trust (tofu_dbs_t dbs, PKT_public_key *pk,
       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)
@@ -1686,8 +1862,12 @@ get_trust (tofu_dbs_t dbs, PKT_public_key *pk,
       goto out;
     }
 
+  /* We can't be in a normal transaction in ask_about_binding.  */
+  end_transaction (ctrl, 0);
+  in_transaction = 0;
+
   /* If we get here, we need to ask the user about the binding.  */
-  ask_about_binding (dbs,
+  ask_about_binding (ctrl,
                      &policy,
                      &trust_level,
                      bindings_with_this_email_count,
@@ -1698,6 +1878,9 @@ get_trust (tofu_dbs_t dbs, PKT_public_key *pk,
                      user_id);
 
  out:
+  if (in_transaction)
+    end_transaction (ctrl, 0);
+
   if (change_conflicting_to_ask)
     {
       if (! may_ask)
@@ -2001,7 +2184,7 @@ show_statistics (tofu_dbs_t dbs, const char *fingerprint,
 
        }
 
-      if (messages == -1 || !first_seen)
+      if (messages == -1 || first_seen == -1)
         {
           write_stats_status (outfp, 0, TOFU_POLICY_NONE, 0, 0);
           if (!outfp)
@@ -2162,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.
@@ -2179,159 +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 (ctrl_t ctrl, 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)
 {
+  gpg_error_t rc;
   tofu_dbs_t dbs;
   char *fingerprint = 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 (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);
-
-  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, pk, fingerprint, email, user_id, may_ask);
-  if (trust_level == _tofu_GET_TRUST_ERROR)
-    /* An error.  */
+  for (user_id = user_id_list; user_id; user_id = user_id->next)
     {
-      trust_level = TRUST_UNKNOWN;
-      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)
-    goto die;
+      email = email_from_user_id (user_id->d);
 
-  /* 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 = ?",
-     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);
+       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_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);
+        (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 (ctrl);
+    rollback_transaction (ctrl);
   else
     rc = end_transaction (ctrl, 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, NULL);
-
-  xfree (email);
   xfree (fingerprint);
   xfree (sig_digest);
 
-  return trust_level;
+  return rc;
 }
 
 /* Combine a trust level returned from the TOFU trust model with a
@@ -2429,55 +2606,109 @@ tofu_write_tfs_record (ctrl_t ctrl, estream_t fp,
 }
 
 
-/* Return the validity (TRUST_NEVER, etc.) of the binding
-   <FINGERPRINT, USER_ID>.
+/* 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 (ctrl_t ctrl, 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)
 {
   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 (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;
+        }
 
-  trust_level = get_trust (dbs, pk, fingerprint, email, user_id, may_ask);
-  if (trust_level == _tofu_GET_TRUST_ERROR)
-    /* An error.  */
-    trust_level = TRUST_UNDEFINED;
+      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 ++;
+
+      if (may_ask && tl != TRUST_ULTIMATE && tl != TRUST_EXPIRED)
+        show_statistics (dbs, fingerprint, email, user_id->d, NULL, 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;
+        }
 
-  if (may_ask && trust_level != TRUST_ULTIMATE)
-    show_statistics (dbs, fingerprint, email, user_id, NULL, NULL);
+      xfree (email);
+    }
 
  die:
-  xfree (email);
+  tofu_end_batch_update (ctrl);
+
   xfree (fingerprint);
+
+  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;
 }
 
@@ -2515,6 +2746,8 @@ tofu_set_policy (ctrl_t ctrl, 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;
@@ -2536,6 +2769,8 @@ tofu_set_policy (ctrl_t ctrl, kbnode_t kb, enum tofu_policy policy)
       xfree (email);
     }
 
+  end_transaction (ctrl, 0);
+
   xfree (fingerprint);
   return 0;
 }