diff options
-rw-r--r-- | configure.ac | 6 | ||||
-rw-r--r-- | doc/DETAILS | 4 | ||||
-rw-r--r-- | doc/gnupg.texi | 3 | ||||
-rw-r--r-- | doc/gpg.texi | 91 | ||||
-rw-r--r-- | g10/Makefile.am | 7 | ||||
-rw-r--r-- | g10/gpg.c | 140 | ||||
-rw-r--r-- | g10/gpgv.c | 28 | ||||
-rw-r--r-- | g10/keyedit.c | 14 | ||||
-rw-r--r-- | g10/keylist.c | 15 | ||||
-rw-r--r-- | g10/mainproc.c | 30 | ||||
-rw-r--r-- | g10/options.h | 10 | ||||
-rw-r--r-- | g10/packet.h | 5 | ||||
-rw-r--r-- | g10/pkclist.c | 11 | ||||
-rw-r--r-- | g10/test-stubs.c | 28 | ||||
-rw-r--r-- | g10/tofu.c | 2472 | ||||
-rw-r--r-- | g10/tofu.h | 105 | ||||
-rw-r--r-- | g10/trust.c | 11 | ||||
-rw-r--r-- | g10/trustdb.c | 180 | ||||
-rw-r--r-- | g10/trustdb.h | 6 | ||||
-rw-r--r-- | tests/openpgp/Makefile.am | 11 | ||||
-rw-r--r-- | tests/openpgp/tofu-2183839A-1.txt | bin | 0 -> 191 bytes | |||
-rw-r--r-- | tests/openpgp/tofu-BC15C85A-1.txt | 9 | ||||
-rw-r--r-- | tests/openpgp/tofu-EE37CF96-1.txt | 9 | ||||
-rwxr-xr-x | tests/openpgp/tofu-keys-secret.asc | 95 | ||||
-rwxr-xr-x | tests/openpgp/tofu-keys.asc | 47 | ||||
-rwxr-xr-x | tests/openpgp/tofu.test | 245 |
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) @@ -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 Binary files differnew file mode 100644 index 000000000..521b3bba4 --- /dev/null +++ b/tests/openpgp/tofu-2183839A-1.txt 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 |