aboutsummaryrefslogtreecommitdiffstats
path: root/g10/tofu.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--g10/tofu.c2472
1 files changed, 2472 insertions, 0 deletions
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, [email protected]> and then we observe
+ <0xbaddecaf, [email protected]>. 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;
+}