summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--configure.ac6
-rw-r--r--doc/DETAILS4
-rw-r--r--doc/gnupg.texi3
-rw-r--r--doc/gpg.texi91
-rw-r--r--g10/Makefile.am7
-rw-r--r--g10/gpg.c140
-rw-r--r--g10/gpgv.c28
-rw-r--r--g10/keyedit.c14
-rw-r--r--g10/keylist.c15
-rw-r--r--g10/mainproc.c30
-rw-r--r--g10/options.h10
-rw-r--r--g10/packet.h5
-rw-r--r--g10/pkclist.c11
-rw-r--r--g10/test-stubs.c28
-rw-r--r--g10/tofu.c2472
-rw-r--r--g10/tofu.h105
-rw-r--r--g10/trust.c11
-rw-r--r--g10/trustdb.c180
-rw-r--r--g10/trustdb.h6
-rw-r--r--tests/openpgp/Makefile.am11
-rw-r--r--tests/openpgp/tofu-2183839A-1.txtbin0 -> 191 bytes
-rw-r--r--tests/openpgp/tofu-BC15C85A-1.txt9
-rw-r--r--tests/openpgp/tofu-EE37CF96-1.txt9
-rwxr-xr-xtests/openpgp/tofu-keys-secret.asc95
-rwxr-xr-xtests/openpgp/tofu-keys.asc47
-rwxr-xr-xtests/openpgp/tofu.test245
26 files changed, 3505 insertions, 77 deletions
diff --git a/configure.ac b/configure.ac
index 289df2a7e..ddbc0657d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -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)
diff --git a/doc/DETAILS b/doc/DETAILS
index 811b1055a..97079b0f0 100644
--- a/doc/DETAILS
+++ b/doc/DETAILS
@@ -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
diff --git a/doc/gnupg.texi b/doc/gnupg.texi
index 1fddeb007..42d9dc00c 100644
--- a/doc/gnupg.texi
+++ b/doc/gnupg.texi
@@ -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
diff --git a/doc/gpg.texi b/doc/gpg.texi
index 35291a821..a70204043 100644
--- a/doc/gpg.texi
+++ b/doc/gpg.texi
@@ -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).
diff --git a/g10/Makefile.am b/g10/Makefile.am
index cd121833a..73578435d 100644
--- a/g10/Makefile.am
+++ b/g10/Makefile.am
@@ -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)
diff --git a/g10/gpg.c b/g10/gpg.c
index 39cc2e5bf..ada913c0a 100644
--- 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:
diff --git a/g10/gpgv.c b/g10/gpgv.c
index 8bb3fc41a..0807622a6 100644
--- a/g10/gpgv.c
+++ b/g10/gpgv.c
@@ -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";
+}
diff --git a/g10/keyedit.c b/g10/keyedit.c
index 4803f9ebe..432ba8663 100644
--- a/g10/keyedit.c
+++ b/g10/keyedit.c
@@ -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*/
diff --git a/g10/keylist.c b/g10/keylist.c
index 3814f1c7c..154169718 100644
--- a/g10/keylist.c
+++ b/g10/keylist.c
@@ -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);
}
diff --git a/g10/mainproc.c b/g10/mainproc.c
index 9f02b1555..af5098717 100644
--- a/g10/mainproc.c
+++ b/g10/mainproc.c
@@ -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
diff --git a/g10/options.h b/g10/options.h
index d57ab5d11..2135aa0b9 100644
--- a/g10/options.h
+++ b/g10/options.h
@@ -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
{
diff --git a/g10/packet.h b/g10/packet.h
index eb7da7520..2c1b478c1 100644
--- a/g10/packet.h
+++ b/g10/packet.h
@@ -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
diff --git a/g10/pkclist.c b/g10/pkclist.c
index 9996d1828..06ba86e08 100644
--- a/g10/pkclist.c
+++ b/g10/pkclist.c
@@ -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") );
diff --git a/g10/test-stubs.c b/g10/test-stubs.c
index f3155fd1f..dba603498 100644
--- a/g10/test-stubs.c
+++ b/g10/test-stubs.c
@@ -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
index 000000000..39377cb14
--- /dev/null
+++ b/g10/tofu.c
@@ -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
index 000000000..75166849e
--- /dev/null
+++ b/g10/tofu.h
@@ -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
diff --git a/g10/trust.c b/g10/trust.c
index 316fe2fe1..38d957e41 100644
--- a/g10/trust.c
+++ b/g10/trust.c
@@ -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);
diff --git a/g10/trustdb.c b/g10/trustdb.c
index b16682da1..170c04122 100644
--- a/g10/trustdb.c
+++ b/g10/trustdb.c
@@ -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;
diff --git a/g10/trustdb.h b/g10/trustdb.h
index 771a821fd..2c3f86553 100644
--- a/g10/trustdb.h
+++ b/g10/trustdb.h
@@ -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,
diff --git a/tests/openpgp/Makefile.am b/tests/openpgp/Makefile.am
index 95bb92c1c..f82fc1d80 100644
--- a/tests/openpgp/Makefile.am
+++ b/tests/openpgp/Makefile.am
@@ -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
index 000000000..521b3bba4
--- /dev/null
+++ b/tests/openpgp/tofu-2183839A-1.txt
Binary files differ
diff --git a/tests/openpgp/tofu-BC15C85A-1.txt b/tests/openpgp/tofu-BC15C85A-1.txt
new file mode 100644
index 000000000..88cc64935
--- /dev/null
+++ b/tests/openpgp/tofu-BC15C85A-1.txt
@@ -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
index 000000000..33a38db2c
--- /dev/null
+++ b/tests/openpgp/tofu-EE37CF96-1.txt
@@ -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
index 000000000..68e0d201c
--- /dev/null
+++ b/tests/openpgp/tofu-keys-secret.asc
@@ -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
index 000000000..2de1cf792
--- /dev/null
+++ b/tests/openpgp/tofu-keys.asc
@@ -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
index 000000000..18c17562c
--- /dev/null
+++ b/tests/openpgp/tofu.test
@@ -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