g10: Add TOFU support.
authorNeal H. Walfield <neal@g10code.com>
Sun, 18 Oct 2015 16:44:05 +0000 (18:44 +0200)
committerNeal H. Walfield <neal@g10code.com>
Sun, 18 Oct 2015 16:45:40 +0000 (18:45 +0200)
* configure.ac: Check for sqlite3.
(SQLITE3_CFLAGS): AC_SUBST it.
(SQLITE3_LIBS): Likewise.
* g10/Makefile.am (AM_CFLAGS): Add $(SQLITE3_CFLAGS).
(gpg2_SOURCES): Add tofu.h and tofu.c.
(gpg2_LDADD): Add $(SQLITE3_LIBS).
* g10/tofu.c: New file.
* g10/tofu.h: New file.
* g10/options.h (trust_model): Define TM_TOFU and TM_TOFU_PGP.
(tofu_db_format): Define.
* g10/packet.h (PKT_signature): Add fields digest and digest_len.
* g10/gpg.c: Include "tofu.h".
(cmd_and_opt_values): Declare aTOFUPolicy, oTOFUDefaultPolicy,
oTOFUDBFormat.
(opts): Add them.
(parse_trust_model): Recognize the tofu and tofu+pgp trust models.
(parse_tofu_policy): New function.
(parse_tofu_db_format): New function.
(main): Initialize opt.tofu_default_policy and opt.tofu_db_format.
Handle aTOFUPolicy, oTOFUDefaultPolicy and oTOFUDBFormat.
* g10/mainproc.c (do_check_sig): If the signature is good, copy the
hash to SIG->DIGEST and set SIG->DIGEST_LEN appropriately.
* g10/trustdb.h (get_validity): Add arguments sig and may_ask.  Update
callers.
(tdb_get_validity_core): Add arguments sig and may_ask.  Update
callers.
* g10/trust.c (get_validity) Add arguments sig and may_ask.  Pass them
to tdb_get_validity_core.
* g10/trustdb.c: Include "tofu.h".
(trust_model_string): Handle TM_TOFU and TM_TOFU_PGP.
(tdb_get_validity_core): Add arguments sig and may_ask.  If
OPT.TRUST_MODEL is TM_TOFU or TM_TOFU_PGP, compute the TOFU trust
level.  Combine it with the computed PGP trust level, if appropriate.
* g10/keyedit.c: Include "tofu.h".
(show_key_with_all_names_colon): If the trust mode is tofu or
tofu+pgp, then show the trust policy.
* g10/keylist.c: Include "tofu.h".
(public_key_list): Also show the PGP stats if the trust model is
TM_TOFU_PGP.
(list_keyblock_colon): If the trust mode is tofu or
tofu+pgp, then show the trust policy.
* g10/pkclist.c: Include "tofu.h".
* g10/gpgv.c (get_validity): Add arguments sig and may_ask.
(enum tofu_policy): Define.
(tofu_get_policy): New stub.
(tofu_policy_str): Likewise.
* g10/test-stubs.c (get_validity): Add arguments sig and may_ask.
(enum tofu_policy): Define.
(tofu_get_policy): New stub.
(tofu_policy_str): Likewise.
* doc/DETAILS: Describe the TOFU Policy field.
* doc/gpg.texi: Document --tofu-set-policy, --trust-model=tofu,
--trust-model=tofu+pgp, --tofu-default-policy and --tofu-db-format.
* tests/openpgp/Makefile.am (TESTS): Add tofu.test.
(TEST_FILES): Add tofu-keys.asc, tofu-keys-secret.asc,
tofu-2183839A-1.txt, tofu-BC15C85A-1.txt and tofu-EE37CF96-1.txt.
(CLEANFILES): Add tofu.db.
(clean-local): Add tofu.d.
* tests/openpgp/tofu.test: New file.
* tests/openpgp/tofu-2183839A-1.txt: New file.
* tests/openpgp/tofu-BC15C85A-1.txt: New file.
* tests/openpgp/tofu-EE37CF96-1.txt: New file.
* tests/openpgp/tofu-keys.asc: New file.
* tests/openpgp/tofu-keys-secret.asc: New file.

--
Signed-off-by: Neal H. Walfield <neal@g10code.com>.
26 files changed:
configure.ac
doc/DETAILS
doc/gnupg.texi
doc/gpg.texi
g10/Makefile.am
g10/gpg.c
g10/gpgv.c
g10/keyedit.c
g10/keylist.c
g10/mainproc.c
g10/options.h
g10/packet.h
g10/pkclist.c
g10/test-stubs.c
g10/tofu.c [new file with mode: 0644]
g10/tofu.h [new file with mode: 0644]
g10/trust.c
g10/trustdb.c
g10/trustdb.h
tests/openpgp/Makefile.am
tests/openpgp/tofu-2183839A-1.txt [new file with mode: 0644]
tests/openpgp/tofu-BC15C85A-1.txt [new file with mode: 0644]
tests/openpgp/tofu-EE37CF96-1.txt [new file with mode: 0644]
tests/openpgp/tofu-keys-secret.asc [new file with mode: 0755]
tests/openpgp/tofu-keys.asc [new file with mode: 0755]
tests/openpgp/tofu.test [new file with mode: 0755]

index 289df2a..ddbc065 100644 (file)
@@ -780,6 +780,12 @@ DL_LIBS=$LIBS
 AC_SUBST(DL_LIBS)
 LIBS="$gnupg_dlopen_save_libs"
 
+# Checks for g10
+
+PKG_CHECK_MODULES(SQLITE3, sqlite3)
+AC_SUBST(SQLITE3_CFLAGS)
+AC_SUBST(SQLITE3_LIBS)
+
 # Checks for g13
 
 AC_PATH_PROG(ENCFS, encfs, /usr/bin/encfs)
index 811b105..97079b0 100644 (file)
@@ -206,6 +206,10 @@ described here.
 
     For pub, sub, sec, and ssb records this field is used for the ECC
     curve name.
+*** Field 18 - TOFU Policy
+
+    This is the TOFU policy.  It is either good, bad, unknown, ask or
+    auto.  This is only shows for uid records.
 
 ** Special fields
 
index 1fddeb0..42d9dc0 100644 (file)
@@ -35,7 +35,8 @@ Published by The GnuPG Project@*
 @end iftex
 
 @copyright{} 2002, 2004, 2005, 2006, 2007, 2010 Free Software Foundation, Inc.@*
-@copyright{} 2013, 2014, 2015 Werner Koch.
+@copyright{} 2013, 2014, 2015 Werner Koch.@*
+@copyright{} 2015 g10code Gmbh.
 
 @quotation
 Permission is granted to copy, distribute and/or modify this document
index 35291a8..a702040 100644 (file)
@@ -525,6 +525,12 @@ Use the source, Luke :-). The output format is still subject to change.
 Pack or unpack an arbitrary input into/from an OpenPGP ASCII armor.
 This is a GnuPG extension to OpenPGP and in general not very useful.
 
+@item --tofu-set-policy @code{auto|good|unknown|bad|ask}  @code{key...}
+@opindex tofu-set-policy
+Set the TOFU policy for all the bindings associated with the specified
+keys.  For more information about the meaning of the policies,
+@pxref{trust-model-tofu}.  The keys may be specified either by their
+fingerprint (preferred) or their keyid.
 
 @c @item --server
 @c @opindex server
@@ -1408,7 +1414,7 @@ don't want to keep your secret keys (or one of them)
 online but still want to be able to check the validity of a given
 recipient's or signator's key.
 
-@item --trust-model @code{pgp|classic|direct|always|auto}
+@item --trust-model @code{pgp|classic|tofu|tofu+pgp|direct|always|auto}
 @opindex trust-model
 Set what trust model GnuPG should follow. The models are:
 
@@ -1424,6 +1430,65 @@ Set what trust model GnuPG should follow. The models are:
   @opindex trust-mode:classic
   This is the standard Web of Trust as introduced by PGP 2.
 
+  @item tofu
+  @opindex trust-mode:tofu
+  @anchor{trust-model-tofu}
+  TOFU stands for Trust On First Use.  In this trust model, the first
+  time a key is seen, it is memorized.  If later another key is seen
+  with a user id with the same email address, a warning is displayed
+  indicating that there is a conflict and that the key might be a
+  forgery and an attempt at a man-in-the-middle attack.
+
+  Because a potential attacker is able to control the email address
+  and thereby circumvent the conflict detection algorithm by using an
+  email address that is similar in appearance to a trusted email
+  address, whenever a message is verified, statistics about the number
+  of messages signed with the key are shown.  In this way, a user can
+  easily identify attacks using fake keys for regular correspondents.
+
+  When compared with the Web of Trust, TOFU offers significantly
+  weaker security guarantees.  In particular, TOFU only helps ensure
+  consistency (that is, that the binding between a key and email
+  address doesn't change).  A major advantage of TOFU is that it
+  requires little maintenance to use correctly.  To use the web of
+  trust properly, you need to actively sign keys and mark users as
+  trusted introducers.  This is a time-consuming process and anecdotal
+  evidence suggests that even security-conscious users rarely take the
+  time to do this thoroughly and instead rely on an ad-hoc TOFU
+  process.
+
+  In the TOFU model, policies are associated with bindings between
+  keys and email addresses (which are extracted from user ids and
+  normalized).  There are five policies, which can be set manually
+  using the @option{--tofu-policy} option.  The default policy can be
+  set using the @option{--tofu-default-policy} policy.
+
+  The TOFU policies are: @code{auto}, @code{good}, @code{unknown},
+  @code{bad} and @code{ask}.  The @code{auto} policy is used by
+  default (unless overridden by @option{--tofu-default-policy}) and
+  marks a binding as marginally trusted.  The @code{good},
+  @code{unknown} and @code{bad} policies mark a binding as fully
+  trusted, as having unknown trust or as having trust never,
+  respectively.  The @code{unknown} policy is useful for just using
+  TOFU to detect conflicts, but to never assign positive trust to a
+  binding.  The final policy, @code{ask} prompts the user to indicate
+  the binding's trust.  If batch mode is enabled (or input is
+  inappropriate in the context), then the user is not prompted and the
+  @code{undefined} trust level is returned.
+
+  @item tofu+pgp
+  @opindex trust-mode:tofu+pgp
+  This trust model combines TOFU with the Web of Trust.  This is done
+  by computing the trust level for each model and then taking the
+  maximum trust level where the trust levels are ordered as follows:
+  @code{unknown < undefined < marginal < fully < ultimate < expired <
+  never}.
+
+  By setting @option{--tofu-default-policy=unknown}, this model can be
+  used to implement the web of trust with TOFU's conflict detection
+  algorithm, but without its assignment of positive trust values,
+  which some security-conscious users don't like.
+
   @item direct
   @opindex trust-mode:direct
   Key validity is set directly by the user and not calculated via the
@@ -1625,6 +1690,30 @@ key signer (defaults to 1).
 Number of marginally trusted users to introduce a new
 key signer (defaults to 3)
 
+@item --tofu-default-policy @code{auto|good|unknown|bad|ask}
+@opindex tofu-default-policy
+The default TOFU policy (defaults to @code{auto}).  For more
+information about the meaning of this option, @xref{trust-model-tofu}.
+
+@item --tofu-db-format @code{auto|split|flat}
+@opindex tofu-default-policy
+The format for the TOFU DB.
+
+The split file format splits the data across many DBs under the
+@code{tofu.d} directory (one per email address and one per key).  This
+makes it easier to automatically synchronize the data using a tool
+such as Unison (@url{https://www.cis.upenn.edu/~bcpierce/unison/}),
+since the individual files change rarely.
+
+The flat file format keeps all of the data in the single file
+@code{tofu.db}.  This format results in better performance.
+
+If set to auto (which is the default), GnuPG will first check for the
+existence of @code{tofu.d} and @code{tofu.db}.  If one of these
+exists, the corresponding format is used.  If neither or both of these
+exist, then GnuPG defaults to the @code{split} format.  In the latter
+case, a warning is emitted.
+
 @item --max-cert-depth @code{n}
 @opindex max-cert-depth
 Maximum depth of a certification chain (default is 5).
index cd12183..7357843 100644 (file)
@@ -26,7 +26,7 @@ AM_CPPFLAGS = -I$(top_srcdir)/common
 
 include $(top_srcdir)/am/cmacros.am
 
-AM_CFLAGS = $(LIBGCRYPT_CFLAGS) \
+AM_CFLAGS = $(SQLITE3_CFLAGS) $(LIBGCRYPT_CFLAGS) \
             $(LIBASSUAN_CFLAGS) $(GPG_ERROR_CFLAGS)
 
 needed_libs = ../kbx/libkeybox.a $(libcommon)
@@ -126,7 +126,8 @@ gpg2_SOURCES  = gpg.c               \
              call-agent.c call-agent.h \
              trust.c $(trust_source) \
              $(card_source) \
-             exec.c exec.h
+             exec.c exec.h \
+             tofu.h tofu.c
 
 gpgv2_SOURCES = gpgv.c           \
              $(common_source)  \
@@ -141,7 +142,7 @@ gpgv2_SOURCES = gpgv.c           \
 
 LDADD =  $(needed_libs) ../common/libgpgrl.a \
          $(ZLIBS) $(LIBINTL) $(CAPLIBS) $(NETLIBS)
-gpg2_LDADD = $(LDADD) $(LIBGCRYPT_LIBS) $(LIBREADLINE) \
+gpg2_LDADD = $(LDADD) $(SQLITE3_LIBS) $(LIBGCRYPT_LIBS) $(LIBREADLINE) \
              $(LIBASSUAN_LIBS) $(GPG_ERROR_LIBS) \
             $(LIBICONV) $(resource_objs) $(extra_sys_libs)
 gpg2_LDFLAGS = $(extra_bin_ldflags)
index 39cc2e5..ada913c 100644 (file)
--- a/g10/gpg.c
+++ b/g10/gpg.c
@@ -59,6 +59,7 @@
 #include "gc-opt-flags.h"
 #include "asshelp.h"
 #include "call-dirmngr.h"
+#include "tofu.h"
 #include "../common/init.h"
 #include "../common/shareddefs.h"
 
@@ -162,6 +163,7 @@ enum cmd_and_opt_values
     aChangePIN,
     aPasswd,
     aServer,
+    aTOFUPolicy,
 
     oTextmode,
     oNoTextmode,
@@ -385,6 +387,8 @@ enum cmd_and_opt_values
     oNoAutostart,
     oPrintPKARecords,
     oPrintDANERecords,
+    oTOFUDefaultPolicy,
+    oTOFUDBFormat,
 
     oNoop
   };
@@ -475,6 +479,8 @@ static ARGPARSE_OPTS opts[] = {
   ARGPARSE_c (aPrimegen, "gen-prime", "@" ),
   ARGPARSE_c (aGenRandom,"gen-random", "@" ),
   ARGPARSE_c (aServer,   "server",  N_("run in server mode")),
+  ARGPARSE_c (aTOFUPolicy, "tofu-policy",
+             N_("|VALUE|set the TOFU policy for a key (good, unknown, bad, ask, auto)")),
 
   ARGPARSE_group (301, N_("@\nOptions:\n ")),
 
@@ -670,6 +676,8 @@ static ARGPARSE_OPTS opts[] = {
   ARGPARSE_s_i (oDefCertLevel, "default-cert-check-level", "@"), /* old */
   ARGPARSE_s_n (oAlwaysTrust, "always-trust", "@"),
   ARGPARSE_s_s (oTrustModel, "trust-model", "@"),
+  ARGPARSE_s_s (oTOFUDefaultPolicy, "tofu-default-policy", "@"),
+  ARGPARSE_s_s (oTOFUDBFormat, "tofu-db-format", "@"),
   ARGPARSE_s_s (oSetFilename, "set-filename", "@"),
   ARGPARSE_s_n (oForYourEyesOnly, "for-your-eyes-only", "@"),
   ARGPARSE_s_n (oNoForYourEyesOnly, "no-for-your-eyes-only", "@"),
@@ -1939,6 +1947,10 @@ parse_trust_model(const char *model)
     opt.trust_model=TM_ALWAYS;
   else if(ascii_strcasecmp(model,"direct")==0)
     opt.trust_model=TM_DIRECT;
+  else if(ascii_strcasecmp(model,"tofu")==0)
+    opt.trust_model=TM_TOFU;
+  else if(ascii_strcasecmp(model,"tofu+pgp")==0)
+    opt.trust_model=TM_TOFU_PGP;
   else if(ascii_strcasecmp(model,"auto")==0)
     opt.trust_model=TM_AUTO;
   else
@@ -1946,6 +1958,41 @@ parse_trust_model(const char *model)
 }
 #endif /*NO_TRUST_MODELS*/
 
+static int
+parse_tofu_policy (const char *policy)
+{
+  if (ascii_strcasecmp (policy, "auto") == 0)
+    return TOFU_POLICY_AUTO;
+  else if (ascii_strcasecmp (policy, "good") == 0)
+    return TOFU_POLICY_GOOD;
+  else if (ascii_strcasecmp (policy, "unknown") == 0)
+    return TOFU_POLICY_UNKNOWN;
+  else if (ascii_strcasecmp (policy, "bad") == 0)
+    return TOFU_POLICY_BAD;
+  else if (ascii_strcasecmp (policy, "ask") == 0)
+    return TOFU_POLICY_ASK;
+  else
+    {
+      log_error (_("unknown TOFU policy '%s'\n"), policy);
+      g10_exit (1);
+    }
+}
+
+static int
+parse_tofu_db_format (const char *db_format)
+{
+  if (ascii_strcasecmp (db_format, "auto") == 0)
+    return TOFU_DB_AUTO;
+  else if (ascii_strcasecmp (db_format, "split") == 0)
+    return TOFU_DB_SPLIT;
+  else if (ascii_strcasecmp (db_format, "flat") == 0)
+    return TOFU_DB_FLAT;
+  else
+    {
+      log_error (_("unknown TOFU DB format '%s'\n"), db_format);
+      g10_exit (1);
+    }
+}
 
 /* This fucntion called to initialized a new control object.  It is
    assumed that this object has been zeroed out before calling this
@@ -2150,6 +2197,8 @@ main (int argc, char **argv)
 #else
     opt.trust_model = TM_AUTO;
 #endif
+    opt.tofu_default_policy = TOFU_POLICY_AUTO;
+    opt.tofu_db_format = TOFU_DB_AUTO;
     opt.mangle_dos_filenames = 0;
     opt.min_cert_level = 2;
     set_screen_dimensions ();
@@ -2372,6 +2421,10 @@ main (int argc, char **argv)
             opt.batch = 1;
             break;
 
+          case aTOFUPolicy:
+            set_cmd (&cmd, pargs.r_opt);
+            break;
+
          case oArmor: opt.armor = 1; opt.no_armor=0; break;
          case oOutput: opt.outfile = pargs.r.ret_str; break;
          case oMaxOutput: opt.max_output = pargs.r.ret_ulong; break;
@@ -2553,6 +2606,12 @@ main (int argc, char **argv)
            parse_trust_model(pargs.r.ret_str);
            break;
 #endif /*!NO_TRUST_MODELS*/
+         case oTOFUDefaultPolicy:
+           opt.tofu_default_policy = parse_tofu_policy (pargs.r.ret_str);
+           break;
+         case oTOFUDBFormat:
+           opt.tofu_db_format = parse_tofu_db_format (pargs.r.ret_str);
+           break;
 
          case oForceOwnertrust:
            log_info(_("Note: %s is not for normal use!\n"),
@@ -4351,6 +4410,87 @@ main (int argc, char **argv)
         gcry_control (GCRYCTL_PRINT_CONFIG, stdout);
         break;
 
+      case aTOFUPolicy:
+       {
+         int policy;
+         int i;
+         KEYDB_HANDLE hd;
+
+         if (argc < 2)
+           wrong_args("--tofu-policy POLICY KEYID [KEYID...]");
+
+         policy = parse_tofu_policy (argv[0]);
+
+         hd = keydb_new ();
+         if (! hd)
+           {
+             log_error (_("Failed to open the keyring DB.\n"));
+             g10_exit (1);
+           }
+
+         for (i = 1; i < argc; i ++)
+           {
+             KEYDB_SEARCH_DESC desc;
+             kbnode_t kb;
+
+             rc = classify_user_id (argv[i], &desc, 0);
+             if (rc)
+               {
+                 log_error (_("Failed to parse '%s'.\n"), argv[i]);
+                 g10_exit (1);
+               }
+
+             if (! (desc.mode == KEYDB_SEARCH_MODE_SHORT_KID
+                    || desc.mode == KEYDB_SEARCH_MODE_LONG_KID
+                    || desc.mode == KEYDB_SEARCH_MODE_FPR16
+                    || desc.mode == KEYDB_SEARCH_MODE_FPR20
+                    || desc.mode == KEYDB_SEARCH_MODE_FPR
+                    || desc.mode == KEYDB_SEARCH_MODE_KEYGRIP))
+               {
+                 log_error (_("'%s' does not appear to be a valid"
+                              " key id, fingerprint or key grip.\n"),
+                            argv[i]);
+                 g10_exit (1);
+               }
+
+             rc = keydb_search_reset (hd);
+             if (rc)
+               {
+                 log_error (_("Failed to reset keyring handle.\n"));
+                 g10_exit (1);
+               }
+
+             rc = keydb_search (hd, &desc, 1, NULL);
+             if (gpg_err_code (rc) == GPG_ERR_NO_PUBKEY)
+               {
+                 log_error (_("Key '%s' is not available\n"), argv[i]);
+                 g10_exit (1);
+               }
+             else if (rc)
+               {
+                 log_error (_("Failed to find key '%s'\n"), argv[i]);
+                 g10_exit (1);
+               }
+
+             rc = keydb_get_keyblock (hd, &kb);
+             if (rc)
+               {
+                 log_error (_("Failed to read key '%s' from the keyring\n"),
+                            argv[i]);
+                 g10_exit (1);
+               }
+
+             merge_keys_and_selfsig (kb);
+
+             if (tofu_set_policy (kb, policy))
+               g10_exit (1);
+           }
+
+         keydb_release (hd);
+
+       }
+       break;
+
       case aListPackets:
        opt.list_packets=2;
       default:
index 8bb3fc4..0807622 100644 (file)
@@ -285,10 +285,13 @@ get_validity_info (PKT_public_key *pk, PKT_user_id *uid)
 }
 
 unsigned int
-get_validity (PKT_public_key *pk, PKT_user_id *uid)
+get_validity (PKT_public_key *pk, PKT_user_id *uid, PKT_signature *sig,
+             int may_ask)
 {
   (void)pk;
   (void)uid;
+  (void)sig;
+  (void)may_ask;
   return 0;
 }
 
@@ -606,3 +609,26 @@ export_pubkey_buffer (ctrl_t ctrl, const char *keyspec, unsigned int options,
   *r_datalen = 0;
   return gpg_error (GPG_ERR_NOT_IMPLEMENTED);
 }
+
+enum tofu_policy
+  {
+    tofu_policy
+  };
+
+gpg_error_t
+tofu_get_policy (PKT_public_key *pk, PKT_user_id *user_id,
+                enum tofu_policy *policy)
+{
+  (void)pk;
+  (void)user_id;
+  (void)policy;
+  return gpg_error (GPG_ERR_GENERAL);
+}
+
+const char *
+tofu_policy_str (enum tofu_policy policy)
+{
+  (void)policy;
+
+  return "unknown";
+}
index 4803f9e..432ba86 100644 (file)
@@ -47,6 +47,7 @@
 #include "keyserver-internal.h"
 #include "call-agent.h"
 #include "host2net.h"
+#include "tofu.h"
 
 static void show_prefs (PKT_user_id * uid, PKT_signature * selfsig,
                        int verbose);
@@ -2927,6 +2928,14 @@ show_key_with_all_names_colon (ctrl_t ctrl, estream_t fp, kbnode_t keyblock)
          if ((node->flag & NODFLG_MARK_A))
            es_putc ('m', fp);
          es_putc (':', fp);
+         if (opt.trust_model == TM_TOFU || opt.trust_model == TM_TOFU_PGP)
+           {
+             enum tofu_policy policy;
+             if (! tofu_get_policy (primary, uid, &policy)
+                 && policy != TOFU_POLICY_NONE)
+               es_fprintf (fp, "%s", tofu_policy_str (policy));
+           }
+         es_putc (':', fp);
          es_putc ('\n', fp);
        }
     }
@@ -3042,7 +3051,8 @@ show_key_with_all_names (ctrl_t ctrl, estream_t fp,
 
              /* Show a warning once */
              if (!did_warn
-                 && (get_validity (pk, NULL) & TRUST_FLAG_PENDING_CHECK))
+                 && (get_validity (pk, NULL, NULL, 0)
+                     & TRUST_FLAG_PENDING_CHECK))
                {
                  did_warn = 1;
                  do_warn = 1;
@@ -5334,7 +5344,7 @@ menu_revuid (KBNODE pub_keyblock)
                /* If the trustdb has an entry for this key+uid then the
                   trustdb needs an update. */
                if (!update_trust
-                   && (get_validity (pk, uid) & TRUST_MASK) >=
+                   && (get_validity (pk, uid, NULL, 0) & TRUST_MASK) >=
                    TRUST_UNDEFINED)
                  update_trust = 1;
 #endif /*!NO_TRUST_MODELS*/
index 3814f1c..1541697 100644 (file)
@@ -43,6 +43,7 @@
 #include "status.h"
 #include "call-agent.h"
 #include "mbox-util.h"
+#include "tofu.h"
 
 
 static void list_all (ctrl_t, int, int);
@@ -99,7 +100,8 @@ public_key_list (ctrl_t ctrl, strlist_t list, int locate_mode)
        es_fprintf (es_stdout, "o");
       if (trust_model != opt.trust_model)
        es_fprintf (es_stdout, "t");
-      if (opt.trust_model == TM_PGP || opt.trust_model == TM_CLASSIC)
+      if (opt.trust_model == TM_PGP || opt.trust_model == TM_CLASSIC
+         || opt.trust_model == TM_TOFU_PGP)
        {
          if (marginals != opt.marginals_needed)
            es_fprintf (es_stdout, "m");
@@ -1067,7 +1069,7 @@ list_keyblock_print (KBNODE keyblock, int secret, int fpr,
      include, but it looks sort of confusing in the listing... */
   if (opt.list_options & LIST_SHOW_VALIDITY)
     {
-      int validity = get_validity (pk, NULL);
+      int validity = get_validity (pk, NULL, NULL, 0);
       es_fprintf (es_stdout, " [%s]", trust_value_to_string (validity));
     }
 #endif
@@ -1438,6 +1440,7 @@ list_keyblock_colon (KBNODE keyblock, int secret, int has_secret, int fpr)
       xfree (curve);
     }
   es_putc (':', es_stdout);            /* End of field 17. */
+  es_putc (':', es_stdout);            /* End of field 18. */
   es_putc ('\n', es_stdout);
 
   print_revokers (es_stdout, pk);
@@ -1495,6 +1498,14 @@ list_keyblock_colon (KBNODE keyblock, int secret, int has_secret, int fpr)
            es_fprintf (es_stdout, "%u %lu", uid->numattribs, uid->attrib_len);
          else
            es_write_sanitized (es_stdout, uid->name, uid->len, ":", NULL);
+         es_fprintf (es_stdout, "::::::::");
+         if (opt.trust_model == TM_TOFU || opt.trust_model == TM_TOFU_PGP)
+           {
+             enum tofu_policy policy;
+             if (! tofu_get_policy (pk, uid, &policy)
+                 && policy != TOFU_POLICY_NONE)
+               es_fprintf (es_stdout, "%s", tofu_policy_str (policy));
+           }
          es_putc (':', es_stdout);
          es_putc ('\n', es_stdout);
        }
index 9f02b15..af50987 100644 (file)
@@ -851,6 +851,7 @@ do_check_sig (CTX c, kbnode_t node, int *is_selfsig,
   PKT_signature *sig;
   gcry_md_hd_t md = NULL;
   gcry_md_hd_t md2 = NULL;
+  gcry_md_hd_t md_good = NULL;
   int algo, rc;
 
   assert (node->pkt->pkttype == PKT_SIGNATURE);
@@ -926,8 +927,21 @@ do_check_sig (CTX c, kbnode_t node, int *is_selfsig,
     return GPG_ERR_SIG_CLASS;
 
   rc = signature_check2 (sig, md, NULL, is_expkey, is_revkey, NULL);
-  if (gpg_err_code (rc) == GPG_ERR_BAD_SIGNATURE && md2)
-    rc = signature_check2 (sig, md2, NULL, is_expkey, is_revkey, NULL);
+  if (! rc)
+    md_good = md;
+  else if (gpg_err_code (rc) == GPG_ERR_BAD_SIGNATURE && md2)
+    {
+      rc = signature_check2 (sig, md2, NULL, is_expkey, is_revkey, NULL);
+      if (! rc)
+       md_good = md2;
+    }
+
+  if (md_good)
+    {
+      unsigned char *buffer = gcry_md_read (md_good, 0);
+      sig->digest_len = gcry_md_get_algo_dlen (map_md_openpgp_to_gcry (algo));
+      memcpy (sig->digest, buffer, sig->digest_len);
+    }
 
   gcry_md_close (md);
   gcry_md_close (md2);
@@ -1848,9 +1862,10 @@ check_sig_and_print (CTX c, kbnode_t node)
 
           assert (pk);
 
-          /* Get it before we print anything to avoid interrupting the
-             output with the "please do a --check-trustdb" line. */
-          valid = get_validity (pk, un->pkt->pkt.user_id);
+         /* Since this is just informational, don't actually ask the
+            user to update any trust information.  (Note: we register
+            the signature later.)  */
+          valid = get_validity (pk, un->pkt->pkt.user_id, NULL, 0);
 
           keyid_str[17] = 0; /* cut off the "[uncertain]" part */
 
@@ -1939,8 +1954,11 @@ check_sig_and_print (CTX c, kbnode_t node)
                   else if (un->pkt->pkt.user_id->is_expired)
                     valid = _("expired");
                   else
+                   /* Since this is just informational, don't
+                      actually ask the user to update any trust
+                      information.  */
                     valid = (trust_value_to_string
-                             (get_validity (pk, un->pkt->pkt.user_id)));
+                             (get_validity (pk, un->pkt->pkt.user_id, sig, 0)));
                   log_printf (" [%s]\n",valid);
                 }
               else
index d57ab5d..2135aa0 100644 (file)
@@ -118,8 +118,16 @@ struct
      we started storing the trust model inside the trustdb. */
   enum
     {
-      TM_CLASSIC=0, TM_PGP=1, TM_EXTERNAL=2, TM_ALWAYS, TM_DIRECT, TM_AUTO
+      TM_CLASSIC=0, TM_PGP=1, TM_EXTERNAL=2,
+      TM_ALWAYS, TM_DIRECT, TM_AUTO, TM_TOFU, TM_TOFU_PGP
     } trust_model;
+  enum
+    {
+      TOFU_DB_AUTO=0, TOFU_DB_SPLIT, TOFU_DB_FLAT
+    } tofu_db_format;
+  /* TOFU_BINDING_BAD, TOFU_BINDING_ASK, TOFU_BINDING_AUTO, or
+     TOFU_BINDING_GOOD.  */
+  int tofu_default_policy;
   int force_ownertrust;
   enum
     {
index eb7da75..2c1b478 100644 (file)
@@ -175,6 +175,11 @@ typedef struct
   subpktarea_t *unhashed;    /* Ditto for unhashed data. */
   byte digest_start[2];      /* First 2 bytes of the digest. */
   gcry_mpi_t  data[PUBKEY_MAX_NSIG];
+  /* The message digest and its length (in bytes).  Note the maximum
+     digest length is 512 bits (64 bytes).  If DIGEST_LEN is 0, then
+     the digest's value has not been saved here.  */
+  byte digest[512 / 8];
+  int digest_len;
 } PKT_signature;
 
 #define ATTRIB_IMAGE 1
index 9996d18..06ba86e 100644 (file)
@@ -37,6 +37,7 @@
 #include "status.h"
 #include "photoid.h"
 #include "i18n.h"
+#include "tofu.h"
 
 #define CONTROL_D ('D' - 'A' + 1)
 
@@ -507,13 +508,13 @@ do_we_trust_pre( PKT_public_key *pk, unsigned int trustlevel )
 
 /****************
  * Check whether we can trust this signature.
- * Returns: Error if we shall not trust this signatures.
+ * Returns an error code if we should not trust this signature.
  */
 int
 check_signatures_trust( PKT_signature *sig )
 {
   PKT_public_key *pk = xmalloc_clear( sizeof *pk );
-  unsigned int trustlevel;
+  unsigned int trustlevel = TRUST_UNKNOWN;
   int rc=0;
 
   rc = get_pubkey( pk, sig->keyid );
@@ -537,7 +538,7 @@ check_signatures_trust( PKT_signature *sig )
     log_info(_("WARNING: this key might be revoked (revocation key"
               " not present)\n"));
 
-  trustlevel = get_validity (pk, NULL);
+  trustlevel = get_validity (pk, NULL, sig, 1);
 
   if ( (trustlevel & TRUST_FLAG_REVOKED) )
     {
@@ -829,7 +830,7 @@ find_and_check_key (ctrl_t ctrl, const char *name, unsigned int use,
     }
 
   /* Key found and usable.  Check validity. */
-  trustlevel = get_validity (pk, pk->user_id);
+  trustlevel = get_validity (pk, pk->user_id, NULL, 1);
   if ( (trustlevel & TRUST_FLAG_DISABLED) )
     {
       /* Key has been disabled. */
@@ -1114,7 +1115,7 @@ build_pk_list (ctrl_t ctrl,
                 { /* Check validity of this key. */
                   int trustlevel;
 
-                  trustlevel = get_validity (pk, pk->user_id);
+                  trustlevel = get_validity (pk, pk->user_id, NULL, 1);
                   if ( (trustlevel & TRUST_FLAG_DISABLED) )
                     {
                       tty_printf (_("Public key is disabled.\n") );
index f3155fd..dba6034 100644 (file)
@@ -104,10 +104,13 @@ get_validity_info (PKT_public_key *pk, PKT_user_id *uid)
 }
 
 unsigned int
-get_validity (PKT_public_key *pk, PKT_user_id *uid)
+get_validity (PKT_public_key *pk, PKT_user_id *uid, PKT_signature *sig,
+             int may_ask)
 {
   (void)pk;
   (void)uid;
+  (void)sig;
+  (void)may_ask;
   return 0;
 }
 
@@ -425,3 +428,26 @@ export_pubkey_buffer (ctrl_t ctrl, const char *keyspec, unsigned int options,
   *r_datalen = 0;
   return gpg_error (GPG_ERR_NOT_IMPLEMENTED);
 }
+
+enum tofu_policy
+  {
+    tofu_policy
+  };
+
+gpg_error_t
+tofu_get_policy (PKT_public_key *pk, PKT_user_id *user_id,
+                enum tofu_policy *policy)
+{
+  (void)pk;
+  (void)user_id;
+  (void)policy;
+  return gpg_error (GPG_ERR_GENERAL);
+}
+
+const char *
+tofu_policy_str (enum tofu_policy policy)
+{
+  (void)policy;
+
+  return "unknown";
+}
diff --git a/g10/tofu.c b/g10/tofu.c
new file mode 100644 (file)
index 0000000..39377cb
--- /dev/null
@@ -0,0 +1,2472 @@
+/* tofu.c - TOFU trust model.
+ * Copyright (C) 2015 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* TODO:
+
+   - Format the fingerprints nicely when printing (similar to gpg
+     --list-keys)
+ */
+
+#include <config.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <assert.h>
+#include <sqlite3.h>
+
+#include "gpg.h"
+#include "types.h"
+#include "logging.h"
+#include "stringhelp.h"
+#include "options.h"
+#include "mbox-util.h"
+#include "i18n.h"
+#include "trustdb.h"
+#include "mkdir_p.h"
+
+#include "tofu.h"
+
+/* 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.
+
+   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,
+   use the getdb() function.  This will either return an existing
+   handle or open a new DB connection, as appropriate.  */
+struct db
+{
+  struct db *next;
+
+  enum db_type type;
+
+  sqlite3 *db;
+
+  /* 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];
+};
+
+const char *
+tofu_policy_str (enum tofu_policy policy)
+{
+  switch (policy)
+    {
+    case TOFU_POLICY_NONE: return "none";
+    case TOFU_POLICY_AUTO: return "auto";
+    case TOFU_POLICY_GOOD: return "good";
+    case TOFU_POLICY_UNKNOWN: return "unknown";
+    case TOFU_POLICY_BAD: return "bad";
+    case TOFU_POLICY_ASK: return "ask";
+    default: return "???";
+    }
+}
+
+/* Convert a binding policy (e.g., TOFU_POLICY_BAD) to a trust level
+   (e.g., TRUST_BAD) in light of the current configuration.  */
+int
+tofu_policy_to_trust_level (enum tofu_policy policy)
+{
+  if (policy == TOFU_POLICY_AUTO)
+    /* If POLICY is AUTO, fallback to OPT.TOFU_DEFAULT_POLICY.  */
+    policy = opt.tofu_default_policy;
+
+  switch (policy)
+    {
+    case TOFU_POLICY_AUTO:
+      /* If POLICY and OPT.TOFU_DEFAULT_POLICY are both AUTO, default
+        to marginal trust.  */
+      return TRUST_MARGINAL;
+    case TOFU_POLICY_GOOD:
+      return TRUST_FULLY;
+    case TOFU_POLICY_UNKNOWN:
+      return TRUST_UNKNOWN;
+    case TOFU_POLICY_BAD:
+      return TRUST_NEVER;
+    case TOFU_POLICY_ASK:
+      return TRUST_UNKNOWN;
+    default:
+      log_bug ("Bad value for trust policy: %d\n",
+              opt.tofu_default_policy);
+      return 0;
+    }
+}
+
+/* This is a convenience function that combines sqlite3_mprintf and
+   sqlite3_exec.  */
+static int
+sqlite3_exec_printf (sqlite3 *db,
+                    int (*callback)(void*,int,char**,char**), void *cookie,
+                    char **errmsg,
+                    const char *sql, ...)
+{
+  va_list ap;
+  int rc;
+  char *sql2;
+
+  va_start (ap, sql);
+  sql2 = sqlite3_vmprintf (sql, ap);
+  va_end (ap);
+
+#if 0
+  log_debug ("tofo db: executing: '%s'\n", sql2);
+#endif
+
+  rc = sqlite3_exec (db, sql2, callback, cookie, errmsg);
+
+  sqlite3_free (sql2);
+
+  return rc;
+}
+
+
+/* Collect results of a select count (*) ...; style query.  Aborts if
+   the argument is not a valid integer (or real of the form X.0).  */
+static int
+get_single_unsigned_long_cb (void *cookie, int argc, char **argv,
+                            char **azColName)
+{
+  unsigned long int *count = cookie;
+  char *tail = NULL;
+
+  (void) azColName;
+
+  assert (argc == 1);
+
+  errno = 0;
+  *count = strtoul (argv[0], &tail, 0);
+  if (errno || ! (strcmp (tail, ".0") == 0 || *tail == '\0'))
+    /* Abort.  */
+    return 1;
+  return 0;
+}
+
+/* We expect a single integer column whose name is "version".  COOKIE
+   must point to an int.  This function always aborts.  On error or a
+   if the version is bad, sets *VERSION to -1.  */
+static int
+version_check_cb (void *cookie, int argc, char **argv, char **azColName)
+{
+  int *version = cookie;
+
+  if (argc != 1 || strcmp (azColName[0], "version") != 0)
+    {
+      *version = -1;
+      return 1;
+    }
+
+  if (strcmp (argv[0], "1") == 0)
+    *version = 1;
+  else
+    {
+      log_error (_("unsupported TOFU DB version: %s\n"), argv[0]);
+      *version = -1;
+    }
+
+  /* Don't run again.  */
+  return 1;
+}
+
+
+/* If the DB is new, initialize it.  Otherwise, check the DB's
+   version.
+
+   Return 0 if the database is okay and 1 otherwise.  */
+static int
+initdb (sqlite3 *db, enum db_type type)
+{
+  char *err = NULL;
+  int rc;
+  unsigned long int count;
+  int version = -1;
+
+  /* If the DB has no tables, then assume this is a new DB that needs
+     to be initialized.  */
+  rc = sqlite3_exec (db,
+                    "select count(*) from sqlite_master where type='table';",
+                    get_single_unsigned_long_cb, &count, &err);
+  if (rc)
+    {
+      log_error (_("error querying TOFU DB's available tables: %s\n"),
+                err);
+      sqlite3_free (err);
+      return 1;
+    }
+  else if (count != 0)
+    /* Assume that the DB is already initialized.  Make sure the
+       version is okay.  */
+    {
+      rc = sqlite3_exec (db, "select version from version;", version_check_cb,
+                        &version, &err);
+      if (rc == SQLITE_ABORT && version == 1)
+       /* Happy, happy, joy, joy.  */
+       {
+         sqlite3_free (err);
+         return 0;
+       }
+      else if (rc == SQLITE_ABORT && version == -1)
+       /* Unsupported version.  */
+       {
+         /* An error message was already displayed.  */
+         sqlite3_free (err);
+         return 1;
+       }
+      else if (rc)
+       /* Some error.  */
+       {
+         log_error (_("error determining TOFU DB's version: %s\n"), err);
+         sqlite3_free (err);
+         return 1;
+       }
+      else
+       /* Unexpected success.  This can only happen if there are no
+          rows.  */
+       {
+         log_error (_("error determining TOFU DB's version: %s\n"),
+                    "select returned 0, but expected ABORT");
+         return 1;
+       }
+    }
+
+  rc = sqlite3_exec (db, "begin transaction;", NULL, NULL, &err);
+  if (rc)
+    {
+      log_error (_("error beginning transaction on TOFU database: %s\n"),
+                err);
+      sqlite3_free (err);
+      return 1;
+    }
+
+  /* Create the version table.  */
+  rc = sqlite3_exec (db,
+                    "create table version (version INTEGER);",
+                    NULL, NULL, &err);
+  if (rc)
+    {
+      log_error (_("error initializing TOFU database (%s): %s\n"),
+                "version", err);
+      sqlite3_free (err);
+      goto out;
+    }
+
+  /* Initialize the version table, which contains a single integer
+     value.  */
+  rc = sqlite3_exec (db,
+                    "insert into version values (1);",
+                    NULL, NULL, &err);
+  if (rc)
+    {
+      log_error (_("error initializing TOFU database (%s): %s\n"),
+                "version, init", err);
+      sqlite3_free (err);
+      goto out;
+    }
+
+  /* The list of <fingerprint, email> bindings and auxiliary data.
+
+       OID is a unique ID identifying this binding (and used by the
+         signatures table, see below).  Note: OIDs will never be
+         reused.
+
+       FINGERPRINT: The key's fingerprint.
+
+       EMAIL: The normalized email address.
+
+       USER_ID: The unmodified user id from which EMAIL was extracted.
+
+       TIME: The time this binding was first observed.
+
+       POLICY: The trust policy (-1, 0, 1, or 2; see the
+         documentation for TOFU_POLICY_BAD, etc. above).
+
+       CONFLICT is either NULL or a fingerprint.  Assume that we have
+         a binding <0xdeadbeef, foo@example.com> and then we observe
+         <0xbaddecaf, foo@example.com>.  There two bindings conflict
+         (they have the same email address).  When we observe the
+         latter binding, we warn the user about the conflict and ask
+         for a policy decision about the new binding.  We also change
+         the old binding's policy to ask if it was auto.  So that we
+         know why this occured, we also set conflict to 0xbaddecaf.
+  */
+  if (type == DB_EMAIL || type == DB_COMBINED)
+    rc = sqlite3_exec_printf
+      (db, NULL, NULL, &err,
+       "create table bindings\n"
+       " (oid INTEGER PRIMARY KEY AUTOINCREMENT,\n"
+       "  fingerprint TEXT, email TEXT, user_id TEXT, time INTEGER,\n"
+       "  policy BOOLEAN CHECK (policy in (%d, %d, %d, %d, %d)),\n"
+       "  conflict STRING,\n"
+       "  unique (fingerprint, email));\n"
+       "create index bindings_fingerprint_email\n"
+       " on bindings (fingerprint, email);\n"
+       "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): %s\n"),
+                "bindings", err);
+      sqlite3_free (err);
+      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,
+                        "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): %s\n"),
+                    "signatures", err);
+         sqlite3_free (err);
+         goto out;
+       }
+    }
+
+ out:
+  if (rc)
+    {
+      rc = sqlite3_exec (db, "rollback;", NULL, NULL, &err);
+      if (rc)
+       {
+         log_error (_("error aborting transaction on TOFU DB: %s\n"),
+                    err);
+         sqlite3_free (err);
+       }
+      return 1;
+    }
+  else
+    {
+      rc = sqlite3_exec (db, "commit transaction;", NULL, NULL, &err);
+      if (rc)
+       {
+         log_error (_("error committing transaction on TOFU DB: %s\n"),
+                    err);
+         sqlite3_free (err);
+         return 1;
+       }
+      return 0;
+    }
+}
+
+static sqlite3 *combined_db;
+
+/* 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)
+{
+  sqlite3 *db;
+  int filename_free = 0;
+  int rc;
+
+  if (opt.tofu_db_format == TOFU_DB_FLAT)
+    {
+      assert (! filename);
+      assert (type == DB_COMBINED);
+
+      if (combined_db)
+       return combined_db;
+
+      filename = make_filename (opt.homedir, "tofu.db", NULL);
+      filename_free = 1;
+    }
+  else
+    assert (type == DB_EMAIL || type == DB_KEY);
+
+  assert (filename);
+
+  rc = sqlite3_open (filename, &db);
+  if (rc)
+    {
+      log_error (_("can't open TOFU DB ('%s'): %s\n"),
+                filename, sqlite3_errmsg (db));
+      /* Even if an error occurs, DB is guaranteed to be valid.  */
+      sqlite3_close (db);
+      db = NULL;
+    }
+
+  if (filename_free)
+    xfree (filename);
+
+  if (db && initdb (db, type))
+    {
+      sqlite3_close (db);
+      db = NULL;
+    }
+
+  if (opt.tofu_db_format == TOFU_DB_FLAT)
+    combined_db = db;
+
+  return 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 sqlite3 *
+getdb (struct db *dbs, const char *name, enum db_type type)
+{
+  struct db *t = NULL;
+  sqlite3 *sqlitedb = NULL;
+  char *name_sanitized = NULL;
+  char *filename = NULL;
+  int i;
+
+  assert (name);
+  assert (type == DB_EMAIL || type == DB_KEY);
+
+  assert (dbs);
+  /* The first entry is always for the combined DB.  */
+  assert (dbs->type == DB_COMBINED);
+  assert (! dbs->name[0]);
+
+  if (opt.tofu_db_format == TOFU_DB_FLAT)
+    /* When using the flat format, we only have a single combined
+       DB.  */
+    {
+      assert (dbs->db);
+      assert (! dbs->next);
+      return dbs->db;
+    }
+  else
+    /* When using the split format the first entry on the DB list is a
+       dummy entry.  */
+    assert (! dbs->db);
+
+  /* We have the split format.  */
+
+  /* Only allow alpha-numeric characters in the filename.  */
+  name_sanitized = xstrdup (name);
+  for (i = 0; name[i]; i ++)
+    {
+      char c = name_sanitized[i];
+      if (! (('a' <= c && c <= 'z')
+            || ('A' <= c && c <= 'Z')
+            || ('0' <= c && c <= '9')))
+       name_sanitized[i] = '_';
+    }
+
+  /* See if the DB is cached.  */
+  for (t = dbs->next; t; t = t->next)
+    if (type == t->type && strcmp (t->name, name_sanitized) == 0)
+      goto out;
+
+  /* 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.  */
+    if (gnupg_mkdir_p (opt.homedir, "tofu.d", type_str, prefix, NULL) != 0)
+      {
+       log_error (_("unable to create directory %s/%s/%s/%s"),
+                  opt.homedir, "tofu.d", type_str, prefix);
+       g10_exit (1);
+      }
+
+    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 (sizeof (struct db) + strlen (name_sanitized));
+  t->type = type;
+  t->db = sqlitedb;
+  strcpy (t->name, name_sanitized);
+
+  /* Insert it immediately after the first element.  */
+  t->next = dbs->next;
+  dbs->next = t;
+
+ out:
+  xfree (filename);
+  xfree (name_sanitized);
+
+  if (! t)
+    return NULL;
+  return t->db;
+}
+
+
+/* Create a new DB meta-handle.  Returns NULL on error.  */
+static struct db *
+opendbs (void)
+{
+  sqlite3 *db = NULL;
+  struct db *dbs;
+
+  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.  Using split format for TOFU DB.\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 DB.\n");
+       }
+      else if (have_tofu_d)
+       {
+         opt.tofu_db_format = TOFU_DB_SPLIT;
+         if (DBG_TRUST)
+           log_debug ("Using split format for TOFU DB.\n");
+       }
+      else
+       {
+         opt.tofu_db_format = TOFU_DB_SPLIT;
+         if (DBG_TRUST)
+           log_debug ("Using split format for TOFU DB.\n");
+       }
+    }
+
+  if (opt.tofu_db_format == TOFU_DB_FLAT)
+    {
+      db = opendb (NULL, DB_COMBINED);
+      if (! db)
+       return NULL;
+    }
+  else
+    /* Create a dummy entry so that we have a handle.  */
+    ;
+
+  dbs = xmalloc_clear (sizeof (*dbs));
+  dbs->db = db;
+  dbs->type = DB_COMBINED;
+
+  return dbs;
+}
+
+/* Release all of the resources associated with a DB meta-handle.  */
+static void
+closedbs (struct db *dbs)
+{
+  struct db *db;
+  struct db *n;
+
+  /* The first entry is always the combined DB.  */
+  assert (dbs->type == DB_COMBINED);
+  if (opt.tofu_db_format == TOFU_DB_FLAT)
+    {
+      /* If we are using the flat format, then there is only ever the
+        combined DB.  */
+      assert (! dbs->next);
+      assert (dbs->db);
+      assert (dbs->db == combined_db);
+    }
+  else
+    /* In the split format, the combined record is just a place holder
+       so that we have a stable handle.  */
+    assert (! dbs->db);
+
+  for (db = dbs; db; db = n)
+    {
+      n = db->next;
+
+      if (combined_db && db->db == combined_db)
+       {
+         assert (opt.tofu_db_format == TOFU_DB_FLAT);
+         assert (dbs == db);
+         assert (db->type == DB_COMBINED);
+         assert (! db->name[0]);
+       }
+      else if (db->db)
+       /* Not the dummy entry.  */
+       {
+         if (dbs == db)
+           /* The first entry.  */
+           {
+             assert (opt.tofu_db_format == TOFU_DB_FLAT);
+             assert (db->type == DB_COMBINED);
+             assert (! db->name[0]);
+           }
+         else
+           /* Not the first entry.  */
+           {
+             assert (opt.tofu_db_format == TOFU_DB_SPLIT);
+             assert (db->type != DB_COMBINED);
+             assert (db->name[0]);
+           }
+
+         sqlite3_close (db->db);
+       }
+      else
+       /* The dummy entry.  */
+       {
+         assert (opt.tofu_db_format == TOFU_DB_SPLIT);
+         assert (dbs == db);
+         assert (db->type == DB_COMBINED);
+         assert (! db->name[0]);
+       }
+
+      xfree (db);
+    }
+}
+
+
+/* Collect results of a select min (foo) ...; style query.  Aborts if
+   the argument is not a valid integer (or real of the form X.0).  */
+static int
+get_single_long_cb (void *cookie, int argc, char **argv, char **azColName)
+{
+  long *count = cookie;
+  char *tail = NULL;
+
+  (void) azColName;
+
+  assert (argc == 1);
+
+  errno = 0;
+  *count = strtol (argv[0], &tail, 0);
+  if (errno || ! (strcmp (tail, ".0") == 0 || *tail == '\0'))
+    /* Abort.  */
+    return 1;
+  return 0;
+}
+
+
+/* Record (or update) a trust policy about a (possibly new)
+   binding.
+
+   If SHOW_OLD is set, the binding's old policy is displayed.  */
+static gpg_error_t
+record_binding (struct db *dbs, const char *fingerprint, const char *email,
+               const char *user_id, enum tofu_policy policy, int show_old)
+{
+  sqlite3 *db_email = NULL, *db_key = NULL;
+  int rc;
+  char *err = NULL;
+  enum tofu_policy policy_old = TOFU_POLICY_NONE;
+
+  if (! (policy == TOFU_POLICY_AUTO
+        || policy == TOFU_POLICY_GOOD
+        || policy == TOFU_POLICY_UNKNOWN
+        || policy == TOFU_POLICY_BAD
+        || 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)
+    return gpg_error (GPG_ERR_GENERAL);
+
+  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)
+       return gpg_error (GPG_ERR_GENERAL);
+
+      rc = sqlite3_exec (db_email, "begin transaction;", NULL, NULL, &err);
+      if (rc)
+       {
+         log_error (_("error beginning transaction on TOFU %s database: %s\n"),
+                    "email", err);
+         sqlite3_free (err);
+         return gpg_error (GPG_ERR_GENERAL);
+       }
+
+      rc = sqlite3_exec (db_key, "begin transaction;", NULL, NULL, &err);
+      if (rc)
+       {
+         log_error (_("error beginning transaction on TOFU %s database: %s\n"),
+                    "key", err);
+         sqlite3_free (err);
+         goto out_revert_one;
+       }
+    }
+
+  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_exec_printf
+       (db_email, get_single_long_cb, &policy_old, &err,
+        "select policy from bindings where fingerprint = %Q and email = %Q",
+        fingerprint, email);
+      if (rc)
+       {
+         log_debug ("TOFU: Error reading from binding database"
+                    " (reading policy for <%s, %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));
+      else
+       log_debug ("Set TOFU trust policy for binding <%s, %s> to %s.\n",
+                  fingerprint, email,
+                  tofu_policy_str (policy));
+    }
+
+  if (policy_old == policy)
+    /* Nothing to do.  */
+    goto out;
+
+  rc = sqlite3_exec_printf
+    (db_email, NULL, NULL, &err,
+     "insert or replace into bindings\n"
+     " (oid, fingerprint, email, user_id, time, policy)\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 = %Q and email = %Q),\n"
+     "  %Q, %Q, %Q, strftime('%%s','now'), %d);",
+     fingerprint, email, fingerprint, email, user_id, policy);
+  if (rc)
+    {
+      log_error (_("error updating TOFU binding database"
+                  " (inserting <%s, %s> = %s): %s\n"),
+                fingerprint, email, tofu_policy_str (policy),
+                err);
+      sqlite3_free (err);
+      goto out;
+    }
+
+  if (db_key)
+    /* We also need to update the key DB.  */
+    {
+      assert (opt.tofu_db_format == TOFU_DB_SPLIT);
+
+      rc = sqlite3_exec_printf
+       (db_key, 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 = %Q and email = %Q),\n"
+        "  %Q, %Q, %Q);",
+        fingerprint, email, fingerprint, email, user_id);
+      if (rc)
+       {
+         log_error (_("error updating TOFU binding database"
+                      " (inserting <%s, %s>): %s\n"),
+                    fingerprint, email, err);
+         sqlite3_free (err);
+         goto out;
+       }
+    }
+  else
+    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.  */
+    {
+      int rc2;
+
+      rc2 = sqlite3_exec_printf (db_key, NULL, NULL, &err,
+                                rc ? "rollback;" : "end transaction;");
+      if (rc2)
+       {
+         log_error (_("error ending transaction on TOFU database: %s\n"),
+                    err);
+         sqlite3_free (err);
+       }
+
+    out_revert_one:
+      rc2 = sqlite3_exec_printf (db_email, NULL, NULL, &err,
+                                rc ? "rollback;" : "end transaction;");
+      if (rc2)
+       {
+         log_error (_("error ending transaction on TOFU database: %s\n"),
+                    err);
+         sqlite3_free (err);
+       }
+    }
+
+  if (rc)
+    return gpg_error (GPG_ERR_GENERAL);
+  return 0;
+}
+
+
+/* Collect the strings returned by a query in a simply string list.
+   Any NULL values are converted to the empty string.
+
+   If a result has 3 rows and each row contains two columns, then the
+   results are added to the list as follows (the value is parentheses
+   is the 1-based index in the final list):
+
+     row 1, col 2 (6)
+     row 1, col 1 (5)
+     row 2, col 2 (4)
+     row 2, col 1 (3)
+     row 3, col 2 (2)
+     row 3, col 1 (1)
+
+   This is because add_to_strlist pushes the results onto the front of
+   the list.  The end result is that the rows are backwards, but the
+   columns are in the expected order.  */
+static int
+strings_collect_cb (void *cookie, int argc, char **argv, char **azColName)
+{
+  int i;
+  strlist_t *strlist = cookie;
+
+  (void) azColName;
+
+  for (i = argc - 1; i >= 0; i --)
+    add_to_strlist (strlist, argv[i] ? argv[i] : "");
+
+  return 0;
+}
+
+/* Auxiliary data structure to collect statistics about
+   signatures.  */
+struct signature_stats
+{
+  struct signature_stats *next;
+
+  /* The user-assigned policy for this binding.  */
+  enum tofu_policy policy;
+
+  /* How long ago the signature was created (rounded to a multiple of
+     TIME_AGO_UNIT_SMALL, etc.).  */
+  long time_ago;
+  /* Number of signatures during this time.  */
+  unsigned long count;
+
+  /* The key that generated this signature.  */
+  char fingerprint[1];
+};
+
+static void
+signature_stats_free (struct signature_stats *stats)
+{
+  while (stats)
+    {
+      struct signature_stats *next = stats->next;
+      xfree (stats);
+      stats = next;
+    }
+}
+
+static void
+signature_stats_prepend (struct signature_stats **statsp,
+                        const char *fingerprint,
+                        enum tofu_policy policy,
+                        long time_ago,
+                        unsigned long count)
+{
+  struct signature_stats *stats =
+    xmalloc (sizeof (*stats) + strlen (fingerprint));
+
+  stats->next = *statsp;
+  *statsp = stats;
+
+  strcpy (stats->fingerprint, fingerprint);
+  stats->policy = policy;
+  stats->time_ago = time_ago;
+  stats->count = count;
+}
+
+
+/* Process rows that contain the four columns:
+
+     <fingerprint, policy, time ago, count>.  */
+static int
+signature_stats_collect_cb (void *cookie, int argc, char **argv,
+                           char **azColName)
+{
+  struct signature_stats **statsp = cookie;
+  char *tail;
+  int i = 0;
+  enum tofu_policy policy;
+  long time_ago;
+  unsigned long count;
+
+  (void) azColName;
+
+  i ++;
+
+  tail = NULL;
+  errno = 0;
+  policy = strtol (argv[i], &tail, 0);
+  if (errno || ! (strcmp (tail, ".0") == 0 || *tail == '\0'))
+    {
+      /* Abort.  */
+      log_error ("%s: Error converting %s to an integer (tail = '%s')\n",
+                __func__, argv[i], tail);
+      return 1;
+    }
+  i ++;
+
+  tail = NULL;
+  errno = 0;
+  time_ago = strtol (argv[i], &tail, 0);
+  if (errno || ! (strcmp (tail, ".0") == 0 || *tail == '\0'))
+    {
+      /* Abort.  */
+      log_error ("%s: Error converting %s to an integer (tail = '%s')\n",
+                __func__, argv[i], tail);
+      return 1;
+    }
+  i ++;
+
+  tail = NULL;
+  errno = 0;
+  count = strtoul (argv[i], &tail, 0);
+  if (errno || ! (strcmp (tail, ".0") == 0 || *tail == '\0'))
+    {
+      /* Abort.  */
+      log_error ("%s: Error converting %s to an integer (tail = '%s')\n",
+                __func__, argv[i], tail);
+      return 1;
+    }
+  i ++;
+
+  assert (argc == i);
+
+  signature_stats_prepend (statsp, argv[0], policy, time_ago, count);
+
+  return 0;
+}
+
+/* The grouping parameters when collecting signature statistics.  */
+
+/* If a message is signed a couple of hours in the future, just assume
+   some clock skew.  */
+#define TIME_AGO_FUTURE_IGNORE (2 * 60 * 60)
+#if 0
+#  define TIME_AGO_UNIT_SMALL 60
+#  define TIME_AGO_UNIT_SMALL_NAME _("minute")
+#  define TIME_AGO_UNIT_SMALL_NAME_PLURAL _("minutes")
+#  define TIME_AGO_MEDIUM_THRESHOLD (60 * TIME_AGO_UNIT_SMALL)
+#  define TIME_AGO_UNIT_MEDIUM (60 * 60)
+#  define TIME_AGO_UNIT_MEDIUM_NAME _("hour")
+#  define TIME_AGO_UNIT_MEDIUM_NAME_PLURAL _("hours")
+#  define TIME_AGO_LARGE_THRESHOLD (24 * 60 * TIME_AGO_UNIT_SMALL)
+#  define TIME_AGO_UNIT_LARGE (24 * 60 * 60)
+#  define TIME_AGO_UNIT_LARGE_NAME _("day")
+#  define TIME_AGO_UNIT_LARGE_NAME_PLURAL _("days")
+#else
+#  define TIME_AGO_UNIT_SMALL (24 * 60 * 60)
+#  define TIME_AGO_UNIT_SMALL_NAME _("day")
+#  define TIME_AGO_UNIT_SMALL_NAME_PLURAL _("days")
+#  define TIME_AGO_MEDIUM_THRESHOLD (4 * TIME_AGO_UNIT_SMALL)
+#  define TIME_AGO_UNIT_MEDIUM (7 * 24 * 60 * 60)
+#  define TIME_AGO_UNIT_MEDIUM_NAME _("week")
+#  define TIME_AGO_UNIT_MEDIUM_NAME_PLURAL _("weeks")
+#  define TIME_AGO_LARGE_THRESHOLD (28 * TIME_AGO_UNIT_SMALL)
+#  define TIME_AGO_UNIT_LARGE (30 * 24 * 60 * 60)
+#  define TIME_AGO_UNIT_LARGE_NAME _("month")
+#  define TIME_AGO_UNIT_LARGE_NAME_PLURAL _("months")
+#endif
+
+/* Convert from seconds to time units.
+
+   Note: T should already be a multiple of TIME_AGO_UNIT_SMALL or
+   TIME_AGO_UNIT_MEDIUM or TIME_AGO_UNIT_LARGE.  */
+signed long
+time_ago_scale (signed long t)
+{
+  if (t < TIME_AGO_UNIT_MEDIUM)
+    return t / TIME_AGO_UNIT_SMALL;
+  if (t < TIME_AGO_UNIT_LARGE)
+    return t / TIME_AGO_UNIT_MEDIUM;
+  return t / TIME_AGO_UNIT_LARGE;
+}
+
+/* Return the appropriate unit (respecting whether it is plural or
+   singular).  */
+const char *
+time_ago_unit (signed long t)
+{
+  signed long t_scaled = time_ago_scale (t);
+
+  if (t < TIME_AGO_UNIT_MEDIUM)
+    {
+      if (t_scaled == 1)
+       return TIME_AGO_UNIT_SMALL_NAME;
+      return TIME_AGO_UNIT_SMALL_NAME_PLURAL;
+    }
+  if (t < TIME_AGO_UNIT_LARGE)
+    {
+      if (t_scaled == 1)
+       return TIME_AGO_UNIT_MEDIUM_NAME;
+      return TIME_AGO_UNIT_MEDIUM_NAME_PLURAL;
+    }
+  if (t_scaled == 1)
+    return TIME_AGO_UNIT_LARGE_NAME;
+  return TIME_AGO_UNIT_LARGE_NAME_PLURAL;
+}
+
+
+#define GET_POLICY_ERROR 100
+
+/* Return the policy for the binding <FINGERPRINT, EMAIL> (email has
+   already been normalized) and any conflict information in *CONFLICT
+   if CONFLICT is not NULL.  Returns GET_POLICY_ERROR if an error
+   occurs.  */
+static enum tofu_policy
+get_policy (struct db *dbs, const char *fingerprint, const char *email,
+           char **conflict)
+{
+  sqlite3 *db;
+  int rc;
+  char *err = NULL;
+  strlist_t strlist = NULL;
+  char *tail = NULL;
+  enum tofu_policy policy = GET_POLICY_ERROR;
+
+  assert (GET_POLICY_ERROR != TOFU_POLICY_NONE
+         && GET_POLICY_ERROR != TOFU_POLICY_AUTO
+         && GET_POLICY_ERROR != TOFU_POLICY_GOOD
+         && GET_POLICY_ERROR != TOFU_POLICY_UNKNOWN
+         && GET_POLICY_ERROR != TOFU_POLICY_BAD
+         && GET_POLICY_ERROR != TOFU_POLICY_ASK);
+
+  db = getdb (dbs, email, DB_EMAIL);
+  if (! db)
+    return 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_exec_printf
+    (db, strings_collect_cb, &strlist, &err,
+     "select policy, conflict from bindings\n"
+     " where fingerprint = %Q and email = %Q",
+     fingerprint, email);
+  if (rc)
+    {
+      log_error (_("error reading from TOFU database"
+                  " (checking for existing bad bindings): %s\n"),
+                err);
+      sqlite3_free (err);
+      goto out;
+    }
+
+  if (strlist_length (strlist) == 0)
+    /* No results.  */
+    {
+      policy = TOFU_POLICY_NONE;
+      goto out;
+    }
+  else if (strlist_length (strlist) != 2)
+    /* The result has the wrong form.  */
+    {
+      log_error (_("error reading from TOFU database"
+                  " (checking for existing bad bindings):"
+                  " expected 2 results, got %d\n"),
+                strlist_length (strlist));
+      goto out;
+    }
+
+  /* The result has the right form.  */
+
+  errno = 0;
+  policy = strtol (strlist->d, &tail, 0);
+  if (errno || *tail != '\0')
+    {
+      log_error (_("error reading from TOFU database: bad value for policy: %s\n"),
+                strlist->d);
+      goto out;
+    }
+
+  if (! (policy == TOFU_POLICY_AUTO
+        || policy == TOFU_POLICY_GOOD
+        || policy == TOFU_POLICY_UNKNOWN
+        || policy == TOFU_POLICY_BAD
+        || policy == TOFU_POLICY_ASK))
+    {
+      log_error (_("TOFU DB is corrupted.  Invalid value for policy (%d).\n"),
+                policy);
+      policy = GET_POLICY_ERROR;
+      goto out;
+    }
+
+
+  /* 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 (conflict)
+    {
+      if (policy == TOFU_POLICY_ASK && *strlist->next->d)
+       *conflict = xstrdup (strlist->next->d);
+      else
+       *conflict = NULL;
+    }
+
+ out:
+  assert (policy == GET_POLICY_ERROR
+         || policy == TOFU_POLICY_NONE
+         || policy == TOFU_POLICY_AUTO
+         || policy == TOFU_POLICY_GOOD
+         || policy == TOFU_POLICY_UNKNOWN
+         || policy == TOFU_POLICY_BAD
+         || policy == TOFU_POLICY_ASK);
+
+  free_strlist (strlist);
+
+  return policy;
+}
+
+#define GET_TRUST_ERROR 100
+
+/* 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 GET_TRUST_ERROR.
+
+   USER_ID is the unadultered 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.  */
+static enum tofu_policy
+get_trust (struct db *dbs, const char *fingerprint, const char *email,
+          const char *user_id, int may_ask)
+{
+  sqlite3 *db;
+  enum tofu_policy policy;
+  char *conflict = NULL;
+  int rc;
+  char *err = 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;
+
+  if (opt.batch)
+    may_ask = 0;
+
+  /* Make sure GET_TRUST_ERROR isn't equal to any of the trust
+     levels.  */
+  assert (GET_TRUST_ERROR != TRUST_UNKNOWN
+         && GET_TRUST_ERROR != TRUST_EXPIRED
+         && GET_TRUST_ERROR != TRUST_UNDEFINED
+         && GET_TRUST_ERROR != TRUST_NEVER
+         && GET_TRUST_ERROR != TRUST_MARGINAL
+         && GET_TRUST_ERROR != TRUST_FULLY
+         && GET_TRUST_ERROR != TRUST_ULTIMATE);
+
+  db = getdb (dbs, email, DB_EMAIL);
+  if (! db)
+    return GET_TRUST_ERROR;
+
+  policy = get_policy (dbs, fingerprint, email, &conflict);
+  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",
+                  fingerprint, email,
+                  tofu_policy_str (opt.tofu_default_policy));
+    }
+  switch (policy)
+    {
+    case TOFU_POLICY_AUTO:
+    case TOFU_POLICY_GOOD:
+    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.  */
+      if (DBG_TRUST)
+       log_debug ("TOFU: Known binding <%s, %s>'s policy: %s\n",
+                  fingerprint, email, tofu_policy_str (policy));
+      trust_level = tofu_policy_to_trust_level (policy);
+      goto out;
+
+    case TOFU_POLICY_ASK:
+      /* We need to ask the user what to do.  Case #1 or #2 below.  */
+      if (! may_ask)
+       {
+         trust_level = TRUST_UNDEFINED;
+         goto out;
+       }
+
+      break;
+
+    case TOFU_POLICY_NONE:
+      /* The binding is new, we need to check for conflicts.  Case #3
+        below.  */
+      break;
+
+    case GET_POLICY_ERROR:
+      trust_level = GET_TRUST_ERROR;
+      goto out;
+
+    default:
+      log_bug ("%s: Impossible value for policy (%d)\n", __func__, policy);
+    }
+
+
+  /* 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).
+   */
+
+  /* Look for conflicts.  This is need 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_exec_printf
+    (db, strings_collect_cb, &bindings_with_this_email, &err,
+     "select distinct fingerprint from bindings where email = %Q;",
+     email);
+  if (rc)
+    {
+      log_error (_("error reading from TOFU database"
+                  " (listing fingerprints): %s\n"),
+                err);
+      sqlite3_free (err);
+      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.  */
+    {
+      /* If we've seen this binding, then we've seen this email and
+        policy couldn't possibly be TOFU_POLICY_NONE.  */
+      assert (policy == TOFU_POLICY_NONE);
+
+      if (DBG_TRUST)
+       log_debug ("TOFU: New binding <%s, %s>, no conflict.\n",
+                  email, fingerprint);
+
+      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 = GET_TRUST_ERROR;
+         goto out;
+       }
+
+      trust_level = tofu_policy_to_trust_level (TOFU_POLICY_AUTO);
+      goto out;
+    }
+
+  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;
+
+  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).  */
+    {
+      assert (policy == TOFU_POLICY_NONE);
+
+      if (record_binding (dbs, fingerprint, email, user_id,
+                         TOFU_POLICY_ASK, 0) != 0)
+       log_error (_("error setting TOFU binding's trust level to %s\n"),
+                  "ask");
+
+      trust_level = TRUST_UNDEFINED;
+      goto out;
+    }
+
+  /* If we get here, we need to 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).
+   */
+  {
+    int is_conflict =
+      ((policy == TOFU_POLICY_NONE && bindings_with_this_email_count > 0)
+       || (policy == TOFU_POLICY_ASK && conflict));
+    estream_t fp;
+    char *binding;
+    int binding_shown;
+    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\n");
+
+    binding = xasprintf ("<%s, %s>", fingerprint, email);
+    binding_shown = 0;
+
+    if (policy == TOFU_POLICY_NONE)
+      {
+       es_fprintf (fp, _("The binding %s is NOT known.  "), binding);
+       binding_shown = 1;
+      }
+    else if (policy == TOFU_POLICY_ASK && conflict)
+      {
+       es_fprintf (fp,
+                   _("%s raised a conflict with this binding.  Since this"
+                     " binding's policy was 'auto', it was changed to 'ask'.  "),
+                   binding);
+       binding_shown = 1;
+      }
+    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).\n\n"),
+               binding_shown ? "" : binding,
+               binding_shown ? "" : " ");
+
+    xfree (binding);
+
+    /* Find other user ids associated with this key and whether the
+       bindings are marked as good or bad.  */
+    {
+      sqlite3 *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_exec_printf
+           (db_key, strings_collect_cb, &other_user_ids, &err,
+            "select user_id, %s from bindings where fingerprint = %Q;",
+            opt.tofu_db_format == TOFU_DB_SPLIT ? "email" : "policy",
+            fingerprint);
+         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;
+
+           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 (policy: %s)\n"),
+                       other_user_id,
+                       tofu_policy_str (other_policy));
+         }
+       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_exec_printf
+      (db, 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 < -%d then -1\n"
+       "         when delta < %d then max(0, round(delta / %d) * %d)\n"
+       "         when delta < %d then round(delta / %d) * %d\n"
+       "         else round(delta / %d) * %d\n"
+       "        end time_ago,\n"
+       "        delta time_ago_raw\n"
+       "       from (select *,\n"
+       "              cast(strftime('%%s','now') - sig_time as real) delta\n"
+       "             from signatures) ss\n"
+       "       left join bindings on ss.binding = bindings.oid)\n"
+       " where email = %Q\n"
+       " group by fingerprint, time_ago\n"
+       /* Make sure the current key is first.  */
+       " order by fingerprint = %Q asc, fingerprint desc, time_ago desc;\n",
+       TIME_AGO_FUTURE_IGNORE,
+       TIME_AGO_MEDIUM_THRESHOLD, TIME_AGO_UNIT_SMALL, TIME_AGO_UNIT_SMALL,
+       TIME_AGO_LARGE_THRESHOLD, TIME_AGO_UNIT_MEDIUM, TIME_AGO_UNIT_MEDIUM,
+       TIME_AGO_UNIT_LARGE, TIME_AGO_UNIT_LARGE,
+       email, fingerprint);
+    if (rc)
+      {
+       strlist_t strlist_iter;
+
+       log_error (_("error gathering signature stats: %s.\n"),
+                  err);
+       sqlite3_free (err);
+       err = NULL;
+
+       es_fprintf
+         (fp, _("The email address (%s) is associated with %d keys:\n"),
+          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 '%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;
+               key = stats_iter->fingerprint;
+               this_key = strcmp (key, fingerprint) == 0;
+               if (this_key)
+                 es_fprintf (fp, _("  %s (this key):"), key);
+               else
+                 es_fprintf (fp, _("  %s (policy: %s):"),
+                             key, tofu_policy_str (stats_iter->policy));
+               es_fprintf (fp, "\n");
+             }
+
+           if (stats_iter->time_ago == -1)
+             es_fprintf (fp, _("    %ld %s signed in the future.\n"),
+                         stats_iter->count,
+                         stats_iter->count == 1
+                         ? _("message") : _("messages"));
+           else if (stats_iter->count == 0)
+             es_fprintf (fp, _("    0 signed messages.\n"));
+           else
+             es_fprintf (fp, _("    %ld %s signed over the past %ld %s.\n"),
+                         stats_iter->count,
+                         stats_iter->count == 1
+                         ? _("message") : _("messages"),
+                         time_ago_scale (stats_iter->time_ago),
+                         time_ago_unit (stats_iter->time_ago));
+         }
+      }
+
+    if (is_conflict)
+      {
+       /* TRANSLATORS: translate the below text.  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.";
+       es_fprintf (fp, "\n%s\n", text);
+      }
+
+    es_fputc ('\n', fp);
+    /* 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");
+    es_fprintf (fp, _("(G)ood/(A)ccept once/(U)nknown/(R)eject once/(B)ad? "));
+
+    /* Add a NUL terminator.  */
+    es_fputc (0, fp);
+    if (es_fclose_snatch (fp, (void **) &prompt, NULL))
+      log_fatal ("error snatching memory stream\n");
+
+    while (1)
+      {
+       char *response;
+
+       if (strlen (choices) != 10)
+         log_bug ("Bad TOFU conflict translation!  Please report.");
+
+       response = cpr_get ("tofu conflict", prompt);
+       trim_spaces (response);
+       cpr_kill_prompt ();
+       if (strlen (response) == 1)
+         {
+           char *choice = strchr (choices, *response);
+           if (choice)
+             {
+               int c = ((size_t) choice - (size_t) choices) / 2;
+               assert (0 <= c && c <= 3);
+
+               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 = GET_TRUST_ERROR;
+
+               break;
+             }
+         }
+       xfree (response);
+      }
+
+    xfree (prompt);
+
+    signature_stats_free (stats);
+  }
+
+ out:
+  if (change_conflicting_to_ask)
+    {
+      rc = sqlite3_exec_printf
+       (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;
+       }
+    }
+
+  xfree (conflict);
+  free_strlist (bindings_with_this_email);
+
+  return trust_level;
+}
+
+static void
+show_statistics (struct db *dbs, const char *fingerprint,
+                const char *email, const char *user_id,
+                const char *sig_exclude)
+{
+  sqlite3 *db;
+  int rc;
+  strlist_t strlist = NULL;
+  char *err = NULL;
+
+  db = getdb (dbs, email, DB_EMAIL);
+  if (! db)
+    return;
+
+  rc = sqlite3_exec_printf
+    (db, strings_collect_cb, &strlist, &err,
+     "select count (*), strftime('%%s','now') - min (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;",
+     fingerprint, email,
+     /* We want either: sig_digest != 'SIG_EXCLUDE' or sig_digest is
+       not NULL.  */
+     sig_exclude ? "!= '" : "is not NULL",
+     sig_exclude ? sig_exclude : "",
+     sig_exclude ? "'" : "");
+  if (rc)
+    {
+      log_error (_("error reading from TOFU database"
+                  " (getting statistics): %s\n"),
+                err);
+      sqlite3_free (err);
+      goto out;
+    }
+
+  if (! strlist)
+    log_info (_("Have never verified a message signed by key %s!\n"),
+             fingerprint);
+  else
+    {
+      char *tail = NULL;
+      signed long messages;
+      signed long first_seen_ago;
+
+      assert (strlist_length (strlist) == 2);
+
+      errno = 0;
+      messages = strtol (strlist->d, &tail, 0);
+      if (errno || *tail != '\0')
+       /* Abort.  */
+       {
+         log_debug ("%s:%d: Couldn't convert %s (messages) to an int: %s.\n",
+                    __func__, __LINE__, strlist->d, strerror (errno));
+         messages = -1;
+       }
+
+      if (messages == 0 && *strlist->next->d == '\0')
+       /* min(NULL) => NULL => "".  */
+       first_seen_ago = -1;
+      else
+       {
+         errno = 0;
+         first_seen_ago = strtol (strlist->next->d, &tail, 0);
+         if (errno || *tail != '\0')
+           /* Abort.  */
+           {
+             log_debug ("%s:%d: Cound't convert %s (first_seen) to an int: %s.\n",
+                        __func__, __LINE__,
+                        strlist->next->d, strerror (errno));
+             first_seen_ago = 0;
+           }
+       }
+
+      if (messages == -1 || first_seen_ago == 0)
+       log_info (_("Failed to collect signature statistics for \"%s\" (key %s)\n"),
+                 user_id, fingerprint);
+      else
+       {
+         enum tofu_policy policy = get_policy (dbs, fingerprint, email, NULL);
+         estream_t fp;
+         char *msg;
+
+         fp = es_fopenmem (0, "rw,samethread");
+         if (! fp)
+           log_fatal ("error creating memory stream\n");
+
+         if (messages == 0)
+           es_fprintf (fp,
+                       _("Verified 0 messages signed by \"%s\""
+                         " (key: %s, policy %s)."),
+                       user_id, fingerprint, tofu_policy_str (policy));
+         else
+           {
+             int years = 0;
+             int months = 0;
+             int days = 0;
+             int hours = 0;
+             int minutes = 0;
+             int seconds = 0;
+
+             /* The number of units that we've printed so far.  */
+             int count = 0;
+             /* The first unit that we printed (year = 0, month = 1,
+                etc.).  */
+             int first = -1;
+             /* The current unit.  */
+             int i = 0;
+
+             es_fprintf (fp,
+                         _("Verified %ld messages signed by \"%s\""
+                           " (key: %s, policy: %s) in the past "),
+                         messages, user_id,
+                         fingerprint, tofu_policy_str (policy));
+
+             /* It would be nice to use a macro to do this, but gettext
+                works on the unpreprocessed code.  */
+#define MIN_SECS (60)
+#define HOUR_SECS (60 * MIN_SECS)
+#define DAY_SECS (24 * HOUR_SECS)
+#define MONTH_SECS (30 * DAY_SECS)
+#define YEAR_SECS (365 * DAY_SECS)
+
+             if (first_seen_ago > YEAR_SECS)
+               {
+                 years = first_seen_ago / YEAR_SECS;
+                 first_seen_ago -= years * YEAR_SECS;
+               }
+             if (first_seen_ago > MONTH_SECS)
+               {
+                 months = first_seen_ago / MONTH_SECS;
+                 first_seen_ago -= months * MONTH_SECS;
+               }
+             if (first_seen_ago > DAY_SECS)
+               {
+                 days = first_seen_ago / DAY_SECS;
+                 first_seen_ago -= days * DAY_SECS;
+               }
+             if (first_seen_ago > HOUR_SECS)
+               {
+                 hours = first_seen_ago / HOUR_SECS;
+                 first_seen_ago -= hours * HOUR_SECS;
+               }
+             if (first_seen_ago > MIN_SECS)
+               {
+                 minutes = first_seen_ago / MIN_SECS;
+                 first_seen_ago -= minutes * MIN_SECS;
+               }
+             seconds = first_seen_ago;
+
+             if (years)
+               {
+                 if (years > 1)
+                   es_fprintf (fp, _("%d years"), years);
+                 else
+                   es_fprintf (fp, _("%d year"), years);
+                 count ++;
+                 first = i;
+               }
+             i ++;
+             if ((first == -1 || i - first <= 3) && months)
+               {
+                 if (count)
+                   es_fprintf (fp, _(", "));
+
+                 if (months > 1)
+                   es_fprintf (fp, _("%d months"), months);
+                 else
+                   es_fprintf (fp, _("%d month"), months);
+                 count ++;
+                 first = i;
+               }
+             i ++;
+             if ((first == -1 || i - first <= 3) && count < 2 && days)
+               {
+                 if (count)
+                   es_fprintf (fp, _(", "));
+
+                 if (days > 1)
+                   es_fprintf (fp, _("%d days"), days);
+                 else
+                   es_fprintf (fp, _("%d day"), days);
+                 count ++;
+                 first = i;
+               }
+             i ++;
+             if ((first == -1 || i - first <= 3) && count < 2 && hours)
+               {
+                 if (count)
+                   es_fprintf (fp, _(", "));
+
+                 if (hours > 1)
+                   es_fprintf (fp, _("%d hours"), hours);
+                 else
+                   es_fprintf (fp, _("%d hour"), hours);
+                 count ++;
+                 first = i;
+               }
+             i ++;
+             if ((first == -1 || i - first <= 3) && count < 2 && minutes)
+               {
+                 if (count)
+                   es_fprintf (fp, _(", "));
+
+                 if (minutes > 1)
+                   es_fprintf (fp, _("%d minutes"), minutes);
+                 else
+                   es_fprintf (fp, _("%d minute"), minutes);
+                 count ++;
+                 first = i;
+               }
+             i ++;
+             if ((first == -1 || i - first <= 3) && count < 2)
+               {
+                 if (count)
+                   es_fprintf (fp, _(", "));
+
+                 if (seconds > 1)
+                   es_fprintf (fp, _("%d seconds"), seconds);
+                 else
+                   es_fprintf (fp, _("%d second"), seconds);
+               }
+
+             es_fprintf (fp, _("."));
+           }
+
+         es_fputc (0, fp);
+         if (es_fclose_snatch (fp, (void **) &msg, NULL))
+           log_fatal ("error snatching memory stream\n");
+
+         log_info ("%s\n", msg);
+
+         if (policy == TOFU_POLICY_AUTO && messages < 10)
+           {
+             char *set_policy_command;
+             const char *text;
+
+             if (messages == 0)
+               log_info (_("Warning: we've have yet to see a message signed by this key!\n"));
+             else if (messages == 1)
+               log_info (_("Warning: we've only seen a single message signed by this key!\n"));
+
+             set_policy_command =
+               xasprintf ("gpg --tofu-policy bad \"%s\"", fingerprint);
+             /* TRANSLATORS: translate the below text.  We don't
+                directly internationalize that text so that we can
+                tweak it without breaking translations.  */
+             text = _("TOFU: few signatures %s");
+             if (strcmp (text, "TOFU: few signatures %s") == 0)
+               text =
+                 "Warning: if this value is unexpectedly low, this might "
+                 "indicate that this key is a forgery!  Carefully examine "
+                 "the email address for small variations (e.g., additional "
+                 "white space).  If the key is suspect, then use '%s' to "
+                 "mark the key as being bad.\n";
+             log_info (text, set_policy_command);
+             free (set_policy_command);
+           }
+       }
+    }
+
+ out:
+  free_strlist (strlist);
+
+  return;
+}
+
+/* Extract the email address from a user id and normalize it.  If the
+   user id doesn't contain an email address, then we use the whole
+   user_id and normalize that.  The returned string must be freed.  */
+static char *
+email_from_user_id (const char *user_id)
+{
+  char *email = mailbox_from_userid (user_id);
+  if (! email)
+    /* Hmm, no email address was provided.  Just take the lower-case
+       version of the whole user id.  It could be a hostname, for
+       instance.  */
+    email = ascii_strlwr (xstrdup (user_id));
+
+  return email;
+}
+
+/* Pretty print a MAX_FINGERPRINT_LEN-byte binary fingerprint into a
+   malloc'd string.  */
+static char *
+fingerprint_pp (const byte *fingerprint_bin)
+{
+  char fingerprint[MAX_FINGERPRINT_LEN * 2 + 1];
+  char *fingerprint_pretty;
+  int space = (/* The characters and the NUL.  */
+              sizeof (fingerprint)
+              /* After every fourth character, we add a space (except
+                 the last).  */
+              + (sizeof (fingerprint) - 1) / 4 - 1
+              /* Half way through we add a second space.  */
+              + 1);
+  int i;
+  int j;
+
+  bin2hex (fingerprint_bin, MAX_FINGERPRINT_LEN, fingerprint);
+
+  fingerprint_pretty = xmalloc (space);
+
+  for (i = 0, j = 0; i < MAX_FINGERPRINT_LEN * 2; i ++)
+    {
+      if (i && i % 4 == 0)
+       fingerprint_pretty[j ++] = ' ';
+      if (i == MAX_FINGERPRINT_LEN * 2 / 2)
+       fingerprint_pretty[j ++] = ' ';
+
+      fingerprint_pretty[j ++] = fingerprint[i];
+    }
+  fingerprint_pretty[j ++] = 0;
+  assert (j == space);
+
+  return fingerprint_pretty;
+}
+
+/* Register the signature with the binding <FINGERPRINT_BIN, USER_ID>.
+   FINGERPRINT must be MAX_FINGERPRINT_LEN bytes long.
+
+   SIG_DIGEST_BIN is the binary representation of the message's
+   digest.  SIG_DIGEST_BIN_LEN is its length.
+
+   SIG_TIME is the time that the signature was generated.
+
+   ORIGIN is a free-formed string describing the origin of the
+   signature.  If this was from an email and the Claws MUA was used,
+   then this should be something like: "email:claws".  If this is
+   NULL, the default is simply "unknown".
+
+   If MAY_ASK is 1, then this function may interact with the user.
+   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 (const byte *fingerprint_bin, const char *user_id,
+              const byte *sig_digest_bin, int sig_digest_bin_len,
+              time_t sig_time, const char *origin, int may_ask)
+{
+  struct db *dbs;
+  sqlite3 *db;
+  char *fingerprint = NULL;
+  char *email = NULL;
+  char *err = NULL;
+  int rc;
+  int trust_level = TRUST_UNKNOWN;
+  char *sig_digest;
+  unsigned long c;
+  int already_verified = 0;
+
+  dbs = opendbs ();
+  if (! dbs)
+    {
+      log_error (_("error opening TOFU DB.\n"));
+      goto die;
+    }
+
+  fingerprint = fingerprint_pp (fingerprint_bin);
+
+  if (! *user_id)
+    {
+      log_debug ("TOFU: user id is empty.  Can't continue.\n");
+      goto die;
+    }
+
+  email = email_from_user_id (user_id);
+
+  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 == GET_TRUST_ERROR)
+    /* An error.  */
+    {
+      trust_level = TRUST_UNKNOWN;
+      goto die;
+    }
+
+  /* Save the observed signature in the DB.  */
+  sig_digest = make_radix64_string (sig_digest_bin, sig_digest_bin_len);
+
+  db = getdb (dbs, email, DB_EMAIL);
+  if (! db)
+    {
+      log_error (_("error opening TOFU DB.\n"));
+      goto die;
+    }
+
+  /* We do a query and then an insert.  Make sure they are atomic
+     by wrapping them in a transaction.  */
+  rc = sqlite3_exec (db, "begin transaction;", NULL, NULL, &err);
+  if (rc)
+    {
+      log_error (_("error beginning transaction on TOFU database: %s\n"), err);
+      sqlite3_free (err);
+      goto die;
+    }
+
+  /* If we've already seen this signature before, then don't add
+     it again.  */
+  rc = sqlite3_exec_printf
+    (db, get_single_unsigned_long_cb, &c, &err,
+     "select count (*)\n"
+     " from signatures left join bindings\n"
+     "  on signatures.binding = bindings.oid\n"
+     " where fingerprint = %Q and email = %Q and sig_time = 0x%lx\n"
+     "  and sig_digest = %Q",
+     fingerprint, email, (unsigned long) sig_time, sig_digest);
+  if (rc)
+    {
+      log_error (_("error reading from signatures database"
+                  " (checking existence): %s\n"),
+                err);
+      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, %s, time: 0x%lx, sig: %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"
+                  " <key: %s, %s, time: 0x%lx, sig: %s, %s>\n",
+                  fingerprint, email, (unsigned long) sig_time,
+                  sig_digest, origin);
+    }
+  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);
+
+      assert (c == 0);
+
+      rc = sqlite3_exec_printf
+       (db, NULL, NULL, &err,
+        "insert into signatures\n"
+        " (binding, sig_digest, origin, sig_time, time)\n"
+        " values\n"
+        " ((select oid from bindings\n"
+        "    where fingerprint = %Q and email = %Q),\n"
+        "  %Q, %Q, 0x%lx, strftime('%%s', 'now'));",
+        fingerprint, email, sig_digest, origin, (unsigned long) sig_time);
+      if (rc)
+       {
+         log_error (_("error updating TOFU DB"
+                      " (inserting into signatures table): %s\n"),
+                    err);
+         sqlite3_free (err);
+       }
+    }
+
+  /* It only matters whether we abort or commit the transaction
+     (so long as we do something) if we execute the insert.  */
+  if (rc)
+    rc = sqlite3_exec (db, "rollback;", NULL, NULL, &err);
+  else
+    rc = sqlite3_exec (db, "commit transaction;", NULL, NULL, &err);
+  if (rc)
+    {
+      log_error (_("error ending transaction on TOFU database: %s\n"), err);
+      sqlite3_free (err);
+      goto die;
+    }
+
+ die:
+  if (may_ask)
+    /* It's only appropriate to show the statistics in an interactive
+       context.  */
+    show_statistics (dbs, fingerprint, email, user_id,
+                    already_verified ? NULL : sig_digest);
+
+  xfree (email);
+  xfree (fingerprint);
+  if (dbs)
+    closedbs (dbs);
+
+  return trust_level;
+}
+
+/* Combine a trust level returned from the TOFU trust model with a
+   trust level returned by the PGP trust model.  This is primarily of
+   interest when the trust model is tofu+pgp (TM_TOFU_PGP).
+
+   This function ors together the upper bits (the values not covered
+   by TRUST_MASK, i.e., TRUST_FLAG_REVOKED, etc.).  */
+int
+tofu_wot_trust_combine (int tofu_base, int wot_base)
+{
+  int tofu = tofu_base & TRUST_MASK;
+  int wot = wot_base & TRUST_MASK;
+  int upper = (tofu_base & ~TRUST_MASK) | (wot_base & ~TRUST_MASK);
+
+  assert (tofu == TRUST_UNKNOWN
+         || tofu == TRUST_EXPIRED
+         || tofu == TRUST_UNDEFINED
+         || tofu == TRUST_NEVER
+         || tofu == TRUST_MARGINAL
+         || tofu == TRUST_FULLY
+         || tofu == TRUST_ULTIMATE);
+  assert (wot == TRUST_UNKNOWN
+         || wot == TRUST_EXPIRED
+         || wot == TRUST_UNDEFINED
+         || wot == TRUST_NEVER
+         || wot == TRUST_MARGINAL
+         || wot == TRUST_FULLY
+         || wot == TRUST_ULTIMATE);
+
+  /* We first consider negative trust policys.  These trump positive
+     trust policies.  */
+  if (tofu == TRUST_NEVER || wot == TRUST_NEVER)
+    /* TRUST_NEVER trumps everything else.  */
+    return upper | TRUST_NEVER;
+  if (tofu == TRUST_EXPIRED || wot == TRUST_EXPIRED)
+    /* TRUST_EXPIRED trumps everything but TRUST_NEVER.  */
+    return upper | TRUST_EXPIRED;
+
+  /* Now we only have positive or neutral trust policies.  We take
+     the max.  */
+  if (tofu == TRUST_ULTIMATE || wot == TRUST_ULTIMATE)
+    return upper | TRUST_ULTIMATE;
+  if (tofu == TRUST_FULLY || wot == TRUST_FULLY)
+    return upper | TRUST_FULLY;
+  if (tofu == TRUST_MARGINAL || wot == TRUST_MARGINAL)
+    return upper | TRUST_MARGINAL;
+  if (tofu == TRUST_UNDEFINED || wot == TRUST_UNDEFINED)
+    return upper | TRUST_UNDEFINED;
+  return upper | TRUST_UNKNOWN;
+}
+
+/* Return the validity (TRUST_NEVER, etc.) of the binding
+   <FINGERPRINT, USER_ID>.
+
+   FINGERPRINT must be a MAX_FINGERPRINT_LEN-byte fingerprint.
+
+   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.
+
+   Returns TRUST_UNDEFINED if an error occurs.  */
+int
+tofu_get_validity (const byte *fingerprint_bin, const char *user_id,
+                  int may_ask)
+{
+  struct db *dbs;
+  char *fingerprint = NULL;
+  char *email = NULL;
+  int trust_level = TRUST_UNDEFINED;
+
+  dbs = opendbs ();
+  if (! dbs)
+    {
+      log_error (_("error opening TOFU DB.\n"));
+      goto die;
+    }
+
+  fingerprint = fingerprint_pp (fingerprint_bin);
+
+  if (! *user_id)
+    {
+      log_debug ("user id is empty.  Can't get TOFU validity for this binding.\n");
+      goto die;
+    }
+
+  email = email_from_user_id (user_id);
+
+  trust_level = get_trust (dbs, fingerprint, email, user_id, may_ask);
+  if (trust_level == GET_TRUST_ERROR)
+    /* An error.  */
+    trust_level = TRUST_UNDEFINED;
+
+  if (may_ask)
+    show_statistics (dbs, fingerprint, email, user_id, NULL);
+
+ die:
+  xfree (email);
+  xfree (fingerprint);
+  if (dbs)
+    closedbs (dbs);
+
+  return trust_level;
+}
+
+/* Set the policy for all non-revoked user ids in the keyblock KB to
+   POLICY.
+
+   If no key is available with the specified key id, then this
+   function returns GPG_ERR_NO_PUBKEY.
+
+   Returns 0 on success and an error code otherwise.  */
+gpg_error_t
+tofu_set_policy (kbnode_t kb, enum tofu_policy policy)
+{
+  struct db *dbs;
+  PKT_public_key *pk;
+  char fingerprint_bin[MAX_FINGERPRINT_LEN];
+  size_t fingerprint_bin_len = sizeof (fingerprint_bin);
+  char *fingerprint = NULL;
+
+  assert (kb->pkt->pkttype == PKT_PUBLIC_KEY);
+  pk = kb->pkt->pkt.public_key;
+
+  dbs = opendbs ();
+  if (! dbs)
+    {
+      log_error (_("error opening TOFU DB.\n"));
+      return gpg_error (GPG_ERR_GENERAL);
+    }
+
+  if (DBG_TRUST)
+    log_debug ("Setting TOFU policy for %s to %s\n",
+              keystr (pk->keyid), tofu_policy_str (policy));
+  if (! (pk->main_keyid[0] == pk->keyid[0]
+        && pk->main_keyid[1] == pk->keyid[1]))
+    log_bug ("%s: Passed a subkey, but expecting a primary key.\n", __func__);
+
+  fingerprint_from_pk (pk, fingerprint_bin, &fingerprint_bin_len);
+  assert (fingerprint_bin_len == sizeof (fingerprint_bin));
+
+  fingerprint = fingerprint_pp (fingerprint_bin);
+
+  for (; kb; kb = kb->next)
+    {
+      PKT_user_id *user_id;
+      char *email;
+
+      if (kb->pkt->pkttype != PKT_USER_ID)
+       continue;
+
+      user_id = kb->pkt->pkt.user_id;
+      if (user_id->is_revoked)
+       /* Skip revoked user ids.  (Don't skip expired user ids, the
+          expiry can be changed.)  */
+       continue;
+
+      email = email_from_user_id (user_id->name);
+
+      record_binding (dbs, fingerprint, email, user_id->name, policy, 1);
+
+      xfree (email);
+    }
+
+  xfree (fingerprint);
+  closedbs (dbs);
+
+  return 0;
+}
+
+/* Set the TOFU policy for all non-revoked user ids in the KEY with
+   the key id KEYID to POLICY.
+
+   If no key is available with the specified key id, then this
+   function returns GPG_ERR_NO_PUBKEY.
+
+   Returns 0 on success and an error code otherwise.  */
+gpg_error_t
+tofu_set_policy_by_keyid (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 the TOFU policy for the specified binding in *POLICY.  If no
+   policy has been set for the binding, sets *POLICY to
+   TOFU_POLICY_NONE.
+
+   PK is a primary public key and USER_ID is a user id.
+
+   Returns 0 on success and an error code otherwise.  */
+gpg_error_t
+tofu_get_policy (PKT_public_key *pk, PKT_user_id *user_id,
+                enum tofu_policy *policy)
+{
+  struct db *dbs;
+  char fingerprint_bin[MAX_FINGERPRINT_LEN];
+  size_t fingerprint_bin_len = sizeof (fingerprint_bin);
+  char *fingerprint;
+  char *email;
+
+  /* Make sure PK is a primary key.  */
+  assert (pk->main_keyid[0] == pk->keyid[0]
+         && pk->main_keyid[1] == pk->keyid[1]);
+
+  dbs = opendbs ();
+  if (! dbs)
+    {
+      log_error (_("error opening TOFU DB.\n"));
+      return gpg_error (GPG_ERR_GENERAL);
+    }
+
+  fingerprint_from_pk (pk, fingerprint_bin, &fingerprint_bin_len);
+  assert (fingerprint_bin_len == sizeof (fingerprint_bin));
+
+  fingerprint = fingerprint_pp (fingerprint_bin);
+
+  email = email_from_user_id (user_id->name);
+
+  *policy = get_policy (dbs, fingerprint, email, NULL);
+
+  xfree (email);
+  xfree (fingerprint);
+  closedbs (dbs);
+
+  if (*policy == GET_POLICY_ERROR)
+    return gpg_error (GPG_ERR_GENERAL);
+  return 0;
+}
diff --git a/g10/tofu.h b/g10/tofu.h
new file mode 100644 (file)
index 0000000..7516684
--- /dev/null
@@ -0,0 +1,105 @@
+/* tofu.h - TOFU trust model.
+ * Copyright (C) 2015 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef G10_TOFU_H
+#define G10_TOFU_H
+
+#include <config.h>
+
+/* For each binding, we have a trust policy.  */
+enum tofu_policy
+  {
+    /* This value can be returned by tofu_get_policy to indicate that
+       there is no policy set for the specified binding.  */
+    TOFU_POLICY_NONE = 0,
+
+    /* We made a default policy decision.  This is only done if there
+       is no conflict with another binding (that is, the email address
+       is not part of another known key).  The default policy is
+       configurable (and specified using: --tofu-default-policy).
+
+       Note: when using the default policy, we save TOFU_POLICY_AUTO
+       with the binding, not the policy that was in effect.  This way,
+       if the user invokes gpg again, but with a different value for
+       --tofu-default-policy, a different decision is made.  */
+    TOFU_POLICY_AUTO = 1,
+
+    /* The user explicitly marked the binding as good.  In this case,
+       we return TRUST_FULLY.  */
+    TOFU_POLICY_GOOD = 2,
+
+    /* The user explicitly marked the binding as unknown.  In this
+       case, we return TRUST_UNKNOWN.  */
+    TOFU_POLICY_UNKNOWN = 3,
+
+    /* The user explicitly marked the binding as bad.  In this case,
+       we always return TRUST_NEVER.  */
+    TOFU_POLICY_BAD = 4,
+
+    /* The user deferred a definitive policy decision about the
+       binding (by selecting accept once or reject once).  The next
+       time we see this binding, we should ask the user what to
+       do.  */
+    TOFU_POLICY_ASK = 5
+  };
+
+/* Return a string representation of a trust policy.  Returns "???" if
+   POLICY is not valid.  */
+const char *tofu_policy_str (enum tofu_policy policy);
+
+/* Convert a binding policy (e.g., TOFU_POLICY_BAD) to a trust level
+   (e.g., TRUST_BAD) in light of the current configuration.  */
+int tofu_policy_to_trust_level (enum tofu_policy policy);
+
+/* Register the binding <FINGERPRINT, USER_ID> and the signature
+   described by SIGS_DIGEST and SIG_TIME, which it generated.  Origin
+   describes where the signed data came from, e.g., "email:claws"
+   (default: "unknown").  If MAY_ASK is 1, then this function may
+   interact with the user in the case of a conflict or if the
+   binding's policy is ask.  This function returns the binding's trust
+   level.  If an error occurs, it returns TRUST_UNKNOWN.  */
+int tofu_register (const byte *fingerprint, const char *user_id,
+                  const byte *sigs_digest, int sigs_digest_len,
+                  time_t sig_time, const char *origin, int may_ask);
+
+/* Combine a trust level returned from the TOFU trust model with a
+   trust level returned by the PGP trust model.  This is primarily of
+   interest when the trust model is tofu+pgp (TM_TOFU_PGP).  */
+int tofu_wot_trust_combine (int tofu, int wot);
+
+/* Determine the validity (TRUST_NEVER, etc.) of the binding
+   <FINGERPRINT, USER_ID>.  If MAY_ASK is 1, then this function may
+   interact with the user.  If not, TRUST_UNKNOWN is returned.  If an
+   error occurs, TRUST_UNDEFINED is returned.  */
+int tofu_get_validity (const byte *fingerprint, const char *user_id,
+                      int may_ask);
+
+/* Set the policy for all non-revoked user ids in the keyblock KB to
+   POLICY.  */
+gpg_error_t tofu_set_policy (kbnode_t kb, enum tofu_policy policy);
+
+/* Set the TOFU policy for all non-revoked users in the key with the
+   key id KEYID to POLICY.  */
+gpg_error_t tofu_set_policy_by_keyid (u32 *keyid, enum tofu_policy policy);
+
+/* Return the TOFU policy for the specified binding in *POLICY.  */
+gpg_error_t tofu_get_policy (PKT_public_key *pk, PKT_user_id *user_id,
+                            enum tofu_policy *policy);
+
+#endif
index 316fe2f..38d957e 100644 (file)
@@ -152,7 +152,7 @@ uid_trust_string_fixed (PKT_public_key *key, PKT_user_id *uid)
     return                         _("[ expired]");
   else if(key)
     {
-      switch (get_validity(key,uid)&TRUST_MASK)
+      switch (get_validity (key, uid, NULL, 0) & TRUST_MASK)
         {
         case TRUST_UNKNOWN:   return _("[ unknown]");
         case TRUST_EXPIRED:   return _("[ expired]");
@@ -298,7 +298,8 @@ check_or_update_trustdb (void)
  * otherwise, a reasonable value for the entire key is returned.
  */
 unsigned int
-get_validity (PKT_public_key *pk, PKT_user_id *uid)
+get_validity (PKT_public_key *pk, PKT_user_id *uid, PKT_signature *sig,
+             int may_ask)
 {
   int rc;
   unsigned int validity;
@@ -330,7 +331,7 @@ get_validity (PKT_public_key *pk, PKT_user_id *uid)
 #ifdef NO_TRUST_MODELS
   validity = TRUST_UNKNOWN;
 #else
-  validity = tdb_get_validity_core (pk, uid, main_pk);
+  validity = tdb_get_validity_core (pk, uid, main_pk, sig, may_ask);
 #endif
 
  leave:
@@ -359,7 +360,7 @@ get_validity_info (PKT_public_key *pk, PKT_user_id *uid)
   if (!pk)
     return '?';  /* Just in case a NULL PK is passed.  */
 
-  trustlevel = get_validity (pk, uid);
+  trustlevel = get_validity (pk, uid, NULL, 0);
   if ((trustlevel & TRUST_FLAG_REVOKED))
     return 'r';
   return trust_letter (trustlevel);
@@ -374,7 +375,7 @@ get_validity_string (PKT_public_key *pk, PKT_user_id *uid)
   if (!pk)
     return "err";  /* Just in case a NULL PK is passed.  */
 
-  trustlevel = get_validity (pk, uid);
+  trustlevel = get_validity (pk, uid, NULL, 0);
   if ((trustlevel & TRUST_FLAG_REVOKED))
     return _("revoked");
   return trust_value_to_string (trustlevel);
index b16682d..170c041 100644 (file)
@@ -40,6 +40,7 @@
 #include "i18n.h"
 #include "tdbio.h"
 #include "trustdb.h"
+#include "tofu.h"
 
 
 typedef struct key_item **KeyHashTable; /* see new_key_hash_table() */
@@ -379,6 +380,8 @@ trust_model_string(void)
     case TM_CLASSIC:  return "classic";
     case TM_PGP:      return "PGP";
     case TM_EXTERNAL: return "external";
+    case TM_TOFU:     return "TOFU";
+    case TM_TOFU_PGP: return "TOFU+PGP";
     case TM_ALWAYS:   return "always";
     case TM_DIRECT:   return "direct";
     default:          return "unknown";
@@ -963,16 +966,21 @@ tdb_check_trustdb_stale (void)
 
 /*
  * Return the validity information for PK.  This is the core of
- * get_validity.
+ * get_validity.  If SIG is not NULL, then the trust is being
+ * evaluated in the context of the provided signature.  This is used
+ * by the TOFU code to record statistics.
  */
 unsigned int
 tdb_get_validity_core (PKT_public_key *pk, PKT_user_id *uid,
-                       PKT_public_key *main_pk)
+                       PKT_public_key *main_pk,
+                      PKT_signature *sig,
+                      int may_ask)
 {
   TRUSTREC trec, vrec;
   gpg_error_t err;
   ulong recno;
-  unsigned int validity;
+  unsigned int tofu_validity = TRUST_UNKNOWN;
+  unsigned int validity = TRUST_UNKNOWN;
 
   init_trustdb ();
 
@@ -993,60 +1001,146 @@ tdb_get_validity_core (PKT_public_key *pk, PKT_user_id *uid,
       goto leave;
     }
 
-  err = read_trust_record (main_pk, &trec);
-  if (err && gpg_err_code (err) != GPG_ERR_NOT_FOUND)
-    {
-      tdbio_invalid ();
-      return 0;
-    }
-  if (gpg_err_code (err) == GPG_ERR_NOT_FOUND)
+  if (opt.trust_model == TM_TOFU || opt.trust_model == TM_TOFU_PGP)
     {
-      /* No record found.  */
-      validity = TRUST_UNKNOWN;
-      goto leave;
-    }
+      kbnode_t user_id_node;
+      int user_ids = 0;
+      int user_ids_expired = 0;
 
-  /* Loop over all user IDs */
-  recno = trec.r.trust.validlist;
-  validity = 0;
-  while (recno)
-    {
-      read_record (recno, &vrec, RECTYPE_VALID);
+      char fingerprint[MAX_FINGERPRINT_LEN];
+      size_t fingerprint_len = sizeof (fingerprint);
+
+      fingerprint_from_pk (main_pk, fingerprint, &fingerprint_len);
+      assert (fingerprint_len == sizeof (fingerprint));
 
-      if(uid)
+      /* If the caller didn't supply a user id then iterate over all
+        uids.  */
+      if (! uid)
+       user_id_node = get_pubkeyblock (main_pk->keyid);
+
+      while (uid
+            || (user_id_node = find_next_kbnode (user_id_node, PKT_USER_ID)))
        {
-         /* If a user ID is given we return the validity for that
-            user ID ONLY.  If the namehash is not found, then there
-            is no validity at all (i.e. the user ID wasn't
-            signed). */
-         if(memcmp(vrec.r.valid.namehash,uid->namehash,20)==0)
+         unsigned int tl;
+         PKT_user_id *user_id;
+
+         if (uid)
+           user_id = uid;
+         else
+           user_id = user_id_node->pkt->pkt.user_id;
+
+         if (user_id->is_revoked || user_id->is_expired)
+           /* If the user id is revoked or expired, then skip it.  */
            {
-             validity=(vrec.r.valid.validity & TRUST_MASK);
-             break;
+             char *s;
+             if (user_id->is_revoked && user_id->is_expired)
+               s = "revoked and expired";
+             else if (user_id->is_revoked)
+               s = "revoked";
+             else
+               s = "expire";
+
+             log_info ("TOFU: Ignoring %s user id (%s)\n", s, user_id->name);
+
+             continue;
            }
+
+         user_ids ++;
+
+         if (sig)
+           tl = tofu_register (fingerprint, user_id->name,
+                               sig->digest, sig->digest_len,
+                               sig->timestamp, "unknown",
+                               may_ask);
+         else
+           tl = tofu_get_validity (fingerprint, user_id->name, may_ask);
+
+         if (tl == TRUST_EXPIRED)
+           user_ids_expired ++;
+         else if (tl == TRUST_UNDEFINED || tl == TRUST_UNKNOWN)
+           ;
+         else if (tl == TRUST_NEVER)
+           tofu_validity = TRUST_NEVER;
+         else
+           {
+             assert (tl == TRUST_MARGINAL
+                     || tl == TRUST_FULLY
+                     || tl == TRUST_ULTIMATE);
+
+             if (tl > tofu_validity)
+               /* XXX: We we really want the max?  */
+               tofu_validity = tl;
+           }
+
+         if (uid)
+           /* If the caller specified a user id, then we stop
+              now.  */
+           break;
        }
-      else
+    }
+
+  if (opt.trust_model == TM_TOFU_PGP
+      || opt.trust_model == TM_CLASSIC
+      || opt.trust_model == TM_PGP)
+    {
+      err = read_trust_record (main_pk, &trec);
+      if (err && gpg_err_code (err) != GPG_ERR_NOT_FOUND)
        {
-         /* If no namehash is given, we take the maximum validity
-            over all user IDs */
-         if ( validity < (vrec.r.valid.validity & TRUST_MASK) )
-           validity = (vrec.r.valid.validity & TRUST_MASK);
+         tdbio_invalid ();
+         return 0;
+       }
+      if (gpg_err_code (err) == GPG_ERR_NOT_FOUND)
+       {
+         /* No record found.  */
+         validity = TRUST_UNKNOWN;
+         goto leave;
        }
 
-      recno = vrec.r.valid.next;
-    }
+      /* Loop over all user IDs */
+      recno = trec.r.trust.validlist;
+      validity = 0;
+      while (recno)
+       {
+         read_record (recno, &vrec, RECTYPE_VALID);
 
-  if ( (trec.r.trust.ownertrust & TRUST_FLAG_DISABLED) )
-    {
-      validity |= TRUST_FLAG_DISABLED;
-      pk->flags.disabled = 1;
+         if(uid)
+           {
+             /* If a user ID is given we return the validity for that
+                user ID ONLY.  If the namehash is not found, then
+                there is no validity at all (i.e. the user ID wasn't
+                signed). */
+             if(memcmp(vrec.r.valid.namehash,uid->namehash,20)==0)
+               {
+                 validity=(vrec.r.valid.validity & TRUST_MASK);
+                 break;
+               }
+           }
+         else
+           {
+             /* If no user ID is given, we take the maximum validity
+                over all user IDs */
+             if (validity < (vrec.r.valid.validity & TRUST_MASK))
+               validity = (vrec.r.valid.validity & TRUST_MASK);
+           }
+
+         recno = vrec.r.valid.next;
+       }
+
+      if ((trec.r.trust.ownertrust & TRUST_FLAG_DISABLED))
+       {
+         validity |= TRUST_FLAG_DISABLED;
+         pk->flags.disabled = 1;
+       }
+      else
+       pk->flags.disabled = 0;
+      pk->flags.disabled_valid = 1;
     }
-  else
-    pk->flags.disabled = 0;
-  pk->flags.disabled_valid = 1;
 
  leave:
-  if (pending_check_trustdb)
+  validity = tofu_wot_trust_combine (tofu_validity, validity);
+
+  if (opt.trust_model != TM_TOFU
+      && pending_check_trustdb)
     validity |= TRUST_FLAG_PENDING_CHECK;
 
   return validity;
index 771a821..2c3f865 100644 (file)
@@ -86,7 +86,8 @@ void revalidation_mark (void);
 void check_trustdb_stale (void);
 void check_or_update_trustdb (void);
 
-unsigned int get_validity (PKT_public_key *pk, PKT_user_id *uid);
+unsigned int get_validity (PKT_public_key *pk, PKT_user_id *uid,
+                          PKT_signature *sig, int may_ask);
 int get_validity_info (PKT_public_key *pk, PKT_user_id *uid);
 const char *get_validity_string (PKT_public_key *pk, PKT_user_id *uid);
 
@@ -120,7 +121,8 @@ void tdb_check_or_update (void);
 int tdb_cache_disabled_value (PKT_public_key *pk);
 
 unsigned int tdb_get_validity_core (PKT_public_key *pk, PKT_user_id *uid,
-                                    PKT_public_key *main_pk);
+                                    PKT_public_key *main_pk,
+                                   PKT_signature *sig, int may_ask);
 
 void list_trust_path( const char *username );
 int enum_cert_paths( void **context, ulong *lid,
index 95bb92c..f82fc1d 100644 (file)
@@ -38,7 +38,8 @@ TESTS = version.test mds.test \
        armdetachm.test detachm.test genkey1024.test \
        conventional.test conventional-mdc.test \
        multisig.test verify.test armor.test \
-       import.test ecc.test 4gb-packet.test finish.test
+       import.test ecc.test 4gb-packet.test tofu.test \
+       finish.test
 
 
 TEST_FILES = pubring.asc secring.asc plain-1o.asc plain-2o.asc plain-3o.asc \
@@ -46,7 +47,9 @@ TEST_FILES = pubring.asc secring.asc plain-1o.asc plain-2o.asc plain-3o.asc \
             pubring.pkr.asc secring.skr.asc secdemo.asc pubdemo.asc \
              gpg.conf.tmpl gpg-agent.conf.tmpl \
             bug537-test.data.asc bug894-test.asc \
-            bug1223-good.asc bug1223-bogus.asc 4gb-packet.asc
+            bug1223-good.asc bug1223-bogus.asc 4gb-packet.asc \
+            tofu-keys.asc tofu-keys-secret.asc \
+            tofu-2183839A-1.txt tofu-BC15C85A-1.txt tofu-EE37CF96-1.txt
 
 data_files = data-500 data-9000 data-32000 data-80000 plain-large
 
@@ -95,10 +98,10 @@ CLEANFILES = prepared.stamp x y yy z out err  $(data_files) \
             *.test.log gpg_dearmor gpg.conf gpg-agent.conf S.gpg-agent \
             pubring.gpg pubring.gpg~ pubring.kbx pubring.kbx~ \
             secring.gpg pubring.pkr secring.skr \
-            gnupg-test.stop random_seed gpg-agent.log
+            gnupg-test.stop random_seed gpg-agent.log tofu.db
 
 clean-local:
-       -rm -rf private-keys-v1.d openpgp-revocs.d
+       -rm -rf private-keys-v1.d openpgp-revocs.d tofu.d
 
 
 # We need to depend on a couple of programs so that the tests don't
diff --git a/tests/openpgp/tofu-2183839A-1.txt b/tests/openpgp/tofu-2183839A-1.txt
new file mode 100644 (file)
index 0000000..521b3bb
Binary files /dev/null and b/tests/openpgp/tofu-2183839A-1.txt differ
diff --git a/tests/openpgp/tofu-BC15C85A-1.txt b/tests/openpgp/tofu-BC15C85A-1.txt
new file mode 100644 (file)
index 0000000..88cc649
--- /dev/null
@@ -0,0 +1,9 @@
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v2
+
+owGbwMvMwMF46tzNaXtET0QxnmZPYgj9/c+Sq2MOCwMjBwMbKxOIy8DFKQBTo/SK
+hWFThVuj19r3R/6VzQkpaZuQx7s3r9BQ46v8KXkjb58dSjmXyr7enlCzb7dg1zE7
+aynbc6YTF+wXZI4IlAgPuLJhUeSXo0+WllxbFXUz39407cv15TcXThLj+3tFkSnZ
+YFXwM9+nfAoHpt6I/ZY96SJT3XFZKzO1jeZNJhZsV4Vfrjp0UmnH3E4A
+=X9WM
+-----END PGP MESSAGE-----
diff --git a/tests/openpgp/tofu-EE37CF96-1.txt b/tests/openpgp/tofu-EE37CF96-1.txt
new file mode 100644 (file)
index 0000000..33a38db
--- /dev/null
@@ -0,0 +1,9 @@
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v2
+
+owGbwMvMwMEY0Tqz9J35+WmMp9mTGEJ//xPk6pjDwsDIwcDGygTiMnBxCsDULFZm
+/sk4S36iQ6FuZZPMPdOSe/rZOxNThTmzvJN4l1qe9XGdlLhtpumfzh0uhRnzT2Xc
+jmra+ZdN9+XBhml//i7v6XrfuWu56OuEI/fXH0i3P5HELb+j++6SO85VemLq/tvO
+hNvWtddvuZ7+z2JJaqnP4wiu2t+sEze/MWKZ9zz+u2FV6a3OIyJxjwA=
+=JMtb
+-----END PGP MESSAGE-----
diff --git a/tests/openpgp/tofu-keys-secret.asc b/tests/openpgp/tofu-keys-secret.asc
new file mode 100755 (executable)
index 0000000..68e0d20
--- /dev/null
@@ -0,0 +1,95 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v2
+
+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
+CQ==
+=2SlE
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/tests/openpgp/tofu-keys.asc b/tests/openpgp/tofu-keys.asc
new file mode 100755 (executable)
index 0000000..2de1cf7
--- /dev/null
@@ -0,0 +1,47 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v2
+
+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
+0v2sv67Ar70GR4vQwMvfCUAbnsVrL05eBjUJ
+=Btw1
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/tests/openpgp/tofu.test b/tests/openpgp/tofu.test
new file mode 100755 (executable)
index 0000000..18c1756
--- /dev/null
@@ -0,0 +1,245 @@
+#!/bin/sh
+
+. $srcdir/defs.inc || exit 3
+
+# set -x
+
+KEYS="2183839A BC15C85A EE37CF96"
+
+# Make sure $srcdir is set.
+if test "x$srcdir" = x
+then
+    echo srcdir environment variable not set!
+    exit 1
+fi
+
+# Make sure $GNUPGHOME is set.
+if test "x$GNUPGHOME" = x
+then
+    echo "GNUPGHOME not set."
+    exit 1
+fi
+
+# Import the test keys.
+$GPG --import $srcdir/tofu-keys.asc
+
+# Make sure the keys are imported.
+for k in $KEYS
+do
+    if ! $GPG --list-keys $k >/dev/null 2>&1
+    then
+       echo Missing key $k
+       exit 1
+    fi
+done
+
+format=auto
+
+debug()
+{
+    echo "$@" >&2
+}
+
+debug_exec()
+{
+    debug "Running GNUPGHOME=$GNUPGHOME $@"
+    ${@:+"$@"}
+}
+
+# $1 is the keyid of the policy to lookup.  Any remaining arguments
+# are simply passed to GPG.
+#
+# This function only supports keys with a single user id.
+getpolicy()
+{
+    keyid=$1
+    if test x$keyid = x
+    then
+       echo No keyid supplied!
+       exit 1
+    fi
+    shift
+
+    policy=$(debug_exec $GPG --tofu-db-format=$format --trust-model=tofu \
+                 --with-colons $@ --list-keys "$keyid" \
+                   | awk -F: '/^uid:/ { print $18 }')
+    if test $(echo "$policy" | wc -l) -ne 1
+    then
+       echo "Got: $policy" >&2
+       echo "error"
+    else
+       case $policy in
+           auto|good|unknown|bad|ask) echo $policy ;;
+           *) echo "error" ;;
+       esac
+    fi
+}
+
+# $1 is the key id
+# $2 is the expected policy
+# The rest are additional options to pass to gpg.
+checkpolicy()
+{
+    debug
+    debug "checkpolicy($@)"
+
+    keyid=$1
+    shift
+    expected_policy=$1
+    shift
+    policy=$(getpolicy "$keyid" ${@:+"$@"})
+    if test "x$policy" != "x$expected_policy"
+    then
+       echo "$keyid: Expected policy to be \`$expected_policy', but got \`$policy'."
+       exit 1
+    fi
+}
+
+# $1 is the keyid of the trust level to lookup.  Any remaining
+# arguments are simply passed to GPG.
+#
+# This function only supports keys with a single user id.
+gettrust()
+{
+    keyid=$1
+    if test x$keyid = x
+    then
+       echo No keyid supplied!
+       exit 1
+    fi
+    shift
+
+    trust=$(debug_exec $GPG --tofu-db-format=$format --trust-model=tofu \
+                --with-colons $@ --list-keys "$keyid" \
+                   | awk -F: '/^pub:/ { print $2 }')
+    if test $(echo "$trust" | wc -l) -ne 1
+    then
+       echo "error"
+    else
+       case $trust in
+           [oidreqnmfuws-]) echo $trust ;;
+           *) echo "Bad trust value: $trust" >&2; echo "error" ;;
+       esac
+    fi
+}
+
+# $1 is the key id
+# $2 is the expected trust level
+# The rest are additional options to pass to gpg.
+checktrust()
+{
+    debug
+    debug "checktrust($@)"
+
+    keyid=$1
+    shift
+    expected_trust=$1
+    shift
+    trust=$(gettrust "$keyid" ${@:+"$@"})
+    if test "x$trust" != "x$expected_trust"
+    then
+       echo "$keyid: Expected trust to be \`$expected_trust', but got \`$trust'."
+       exit 1
+    fi
+}
+
+# Set key $1's policy to $2.  Any remaining arguments are passed as
+# options to gpg.
+setpolicy()
+{
+    debug
+    debug "setpolicy($@)"
+
+    keyid=$1
+    shift
+    policy=$1
+    shift
+
+    debug_exec $GPG --tofu-db-format=$format \
+        --trust-model=tofu ${@:+"$@"} --tofu-policy $policy $keyid
+}
+
+for format in split flat
+do
+    debug
+    debug "Testing with db format $format"
+
+    # Carefully remove the TOFU db.
+    test -e $GNUPGHOME/tofu.db && rm $GNUPGHOME/tofu.db
+    test -e $GNUPGHOME/tofu.d/email && rm -r $GNUPGHOME/tofu.d/email
+    test -e $GNUPGHOME/tofu.d/key && rm -r $GNUPGHOME/tofu.d/key
+    # This will fail if the directory is not empty.
+    test -e $GNUPGHOME/tofu.d && rmdir $GNUPGHOME/tofu.d
+
+    # Verify a message.  There should be no conflict and the trust policy
+    # should be set to auto.
+    debug_exec $GPG --tofu-db-format=$format --trust-model=tofu \
+        --verify $srcdir/tofu-2183839A-1.txt
+
+    checkpolicy 2183839A auto
+
+    trust=$(gettrust 2183839A)
+    debug "default trust = $trust"
+    if test "x$trust" != xm
+    then
+       echo "Wrong default trust.  Got: \`$trust', expected \`m'"
+       exit 1
+    fi
+
+    # Trust should be derived lazily.  Thus, if the policy is set to auto
+    # and we change --tofu-default-policy, then the trust should change as
+    # well.  Try it.
+    checktrust 2183839A f --tofu-default-policy=good
+    checktrust 2183839A - --tofu-default-policy=unknown
+    checktrust 2183839A n --tofu-default-policy=bad
+
+    # Change the policy to something other than auto and make sure the
+    # policy and the trust are correct.
+    for policy in good unknown bad
+    do
+       if test $policy = good
+       then
+           expected_trust='f'
+       elif test $policy = unknown
+       then
+           expected_trust='-'
+       else
+           expected_trust='n'
+       fi
+
+       debug
+       debug "Setting TOFU policy to $policy"
+       setpolicy 2183839A $policy
+
+       # Since we have a fixed policy, the trust level shouldn't
+       # change if we change the default policy.
+       for default_policy in auto good unknown bad ask
+       do
+           checkpolicy 2183839A $policy --tofu-default-policy=$default_policy
+           checktrust 2183839A $expected_trust \
+                      --tofu-default-policy=$default_policy
+       done
+    done
+
+    # BC15C85A conflicts with 2183839A.  On conflict, this will set
+    # BC15C85A to ask.  If 2183839A is auto (it's not, it's bad), then
+    # it will be set to ask.
+    debug_exec $GPG --tofu-db-format=$format --trust-model=tofu \
+        --verify $srcdir/tofu-BC15C85A-1.txt
+    checkpolicy BC15C85A ask
+    checkpolicy 2183839A bad
+
+    # EE37CF96 conflicts with 2183839A and BC15C85A.  We change
+    # BC15C85A's policy to auto and leave 2183839A's policy at bad.
+    # This conflict should cause BC15C85A's policy to be changed to
+    # ask (since it is auto), but not affect 2183839A's policy.
+    setpolicy BC15C85A auto
+    checkpolicy BC15C85A auto
+    debug_exec $GPG --tofu-db-format=$format --trust-model=tofu \
+        --verify $srcdir/tofu-EE37CF96-1.txt
+    checkpolicy BC15C85A ask
+    checkpolicy 2183839A bad
+    checkpolicy EE37CF96 ask
+done
+
+exit 0