/* tofu.c - TOFU trust model. * Copyright (C) 2015, 2016 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 . */ /* TODO: - Format the fingerprints nicely when printing (similar to gpg --list-keys) */ #include #include #include #include #include #include "gpg.h" #include "types.h" #include "logging.h" #include "stringhelp.h" #include "options.h" #include "mbox-util.h" #include "i18n.h" #include "ttyio.h" #include "trustdb.h" #include "mkdir_p.h" #include "gpgsql.h" #include "status.h" #include "tofu.h" #define CONTROL_L ('L' - 'A' + 1) /* Number of signed messages required to indicate that enough history * is available for basic trust. */ #define BASIC_TRUST_THRESHOLD 10 /* Number of signed messages required to indicate that a lot of * history is available. */ #define FULL_TRUST_THRESHOLD 100 /* An struct with data pertaining to the tofu DB. To initialize this data structure, call opendbs(). Cleanup is done when the CTRL object is released. 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 tofu_dbs_s { sqlite3 *db; struct { sqlite3_stmt *savepoint_batch; sqlite3_stmt *savepoint_batch_commit; sqlite3_stmt *record_binding_get_old_policy; sqlite3_stmt *record_binding_update; sqlite3_stmt *record_binding_update2; sqlite3_stmt *get_policy_select_policy_and_conflict; sqlite3_stmt *get_trust_bindings_with_this_email; sqlite3_stmt *get_trust_gather_other_user_ids; sqlite3_stmt *get_trust_gather_other_keys; sqlite3_stmt *register_already_seen; sqlite3_stmt *register_insert; } s; int in_batch_transaction; int in_transaction; time_t batch_update_started; }; #define STRINGIFY(s) STRINGIFY2(s) #define STRINGIFY2(s) #s /* 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_MEDIUM_THRESHOLD (60 * TIME_AGO_UNIT_SMALL) # define TIME_AGO_UNIT_MEDIUM (60 * 60) # define TIME_AGO_LARGE_THRESHOLD (24 * 60 * TIME_AGO_UNIT_SMALL) # define TIME_AGO_UNIT_LARGE (24 * 60 * 60) #else # define TIME_AGO_UNIT_SMALL (24 * 60 * 60) # define TIME_AGO_MEDIUM_THRESHOLD (4 * TIME_AGO_UNIT_SMALL) # define TIME_AGO_UNIT_MEDIUM (7 * 24 * 60 * 60) # define TIME_AGO_LARGE_THRESHOLD (28 * TIME_AGO_UNIT_SMALL) # define TIME_AGO_UNIT_LARGE (30 * 24 * 60 * 60) #endif /* Local prototypes. */ static gpg_error_t end_transaction (ctrl_t ctrl, int only_batch); static char *email_from_user_id (const char *user_id); 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; } } /* Start a transaction on DB. If ONLY_BATCH is set, then this will start a batch transaction if we haven't started a batch transaction and one has been requested. */ static gpg_error_t begin_transaction (ctrl_t ctrl, int only_batch) { tofu_dbs_t dbs = ctrl->tofu.dbs; int rc; char *err = NULL; log_assert (dbs); /* If we've been in batch update mode for a while (on average, more * than 500 ms), to prevent starving other gpg processes, we drop * and retake the batch lock. * * Note: if we wanted higher resolution, we could use * npth_clock_gettime. */ if (/* No real transactions. */ dbs->in_transaction == 0 /* There is an open batch transaction. */ && dbs->in_batch_transaction /* And some time has gone by since it was started. */ && dbs->batch_update_started != gnupg_get_time ()) { /* If we are in a batch update, then batch updates better have been enabled. */ log_assert (ctrl->tofu.batch_updated_wanted); end_transaction (ctrl, 2); /* Yield to allow another process a chance to run. */ gpgrt_yield (); } if (/* Batch mode is enabled. */ ctrl->tofu.batch_updated_wanted /* But we don't have an open batch transaction. */ && !dbs->in_batch_transaction) { /* We are in batch mode, but we don't have an open batch * transaction. Since the batch save point must be the outer * save point, it must be taken before the inner save point. */ log_assert (dbs->in_transaction == 0); rc = gpgsql_stepx (dbs->db, &dbs->s.savepoint_batch, NULL, NULL, &err, "savepoint batch;", GPGSQL_ARG_END); if (rc) { log_error (_("error beginning transaction on TOFU database: %s\n"), err); sqlite3_free (err); return gpg_error (GPG_ERR_GENERAL); } dbs->in_batch_transaction = 1; dbs->batch_update_started = gnupg_get_time (); } if (only_batch) return 0; log_assert(dbs->in_transaction >= 0); dbs->in_transaction ++; rc = gpgsql_exec_printf (dbs->db, NULL, NULL, &err, "savepoint inner%d;", dbs->in_transaction); if (rc) { log_error (_("error beginning transaction on TOFU database: %s\n"), err); sqlite3_free (err); return gpg_error (GPG_ERR_GENERAL); } return 0; } /* Commit a transaction. If ONLY_BATCH is 1, then this only ends the * batch transaction if we have left batch mode. If ONLY_BATCH is 2, * this ends any open batch transaction even if we are still in batch * mode. */ static gpg_error_t end_transaction (ctrl_t ctrl, int only_batch) { tofu_dbs_t dbs = ctrl->tofu.dbs; int rc; char *err = NULL; if (only_batch) { if (!dbs) return 0; /* Shortcut to allow for easier cleanup code. */ /* If we are releasing the batch transaction, then we better not be in a normal transaction. */ log_assert (dbs->in_transaction == 0); if (/* Batch mode disabled? */ (!ctrl->tofu.batch_updated_wanted || only_batch == 2) /* But, we still have an open batch transaction? */ && dbs->in_batch_transaction) { /* The batch transaction is still in open, but we've left * batch mode. */ dbs->in_batch_transaction = 0; rc = gpgsql_stepx (dbs->db, &dbs->s.savepoint_batch_commit, NULL, NULL, &err, "release batch;", GPGSQL_ARG_END); if (rc) { log_error (_("error committing transaction on TOFU database: %s\n"), err); sqlite3_free (err); return gpg_error (GPG_ERR_GENERAL); } return 0; } return 0; } log_assert (dbs); log_assert (dbs->in_transaction > 0); rc = gpgsql_exec_printf (dbs->db, NULL, NULL, &err, "release inner%d;", dbs->in_transaction); dbs->in_transaction --; if (rc) { log_error (_("error committing transaction on TOFU database: %s\n"), err); sqlite3_free (err); return gpg_error (GPG_ERR_GENERAL); } return 0; } static gpg_error_t rollback_transaction (ctrl_t ctrl) { tofu_dbs_t dbs = ctrl->tofu.dbs; int rc; char *err = NULL; log_assert (dbs); log_assert (dbs->in_transaction > 0); /* Be careful to not any progress made by closed transactions in batch mode. */ rc = gpgsql_exec_printf (dbs->db, NULL, NULL, &err, "rollback to inner%d;", dbs->in_transaction); dbs->in_transaction --; if (rc) { log_error (_("error rolling back transaction on TOFU database: %s\n"), err); sqlite3_free (err); return gpg_error (GPG_ERR_GENERAL); } return 0; } void tofu_begin_batch_update (ctrl_t ctrl) { ctrl->tofu.batch_updated_wanted ++; } void tofu_end_batch_update (ctrl_t ctrl) { log_assert (ctrl->tofu.batch_updated_wanted > 0); ctrl->tofu.batch_updated_wanted --; end_transaction (ctrl, 1); } /* Suspend any extant batch transaction (it is safe to call this even no batch transaction has been started). Note: you cannot suspend a batch transaction if you are in a normal transaction. The batch transaction can be resumed explicitly by calling tofu_resume_batch_transaction or implicitly by starting a normal transaction. */ static void tofu_suspend_batch_transaction (ctrl_t ctrl) { end_transaction (ctrl, 2); } /* Resume a batch transaction if there is no extant batch transaction and one has been requested using tofu_begin_batch_transaction. */ static void tofu_resume_batch_transaction (ctrl_t ctrl) { begin_transaction (ctrl, 1); } /* Wrapper around strtol which prints a warning in case of a * conversion error. On success the converted value is stored at * R_VALUE and 0 is returned; on error FALLBACK is stored at R_VALUE * and an error code is returned. */ static gpg_error_t string_to_long (long *r_value, const char *string, long fallback, int line) { gpg_error_t err; char *tail = NULL; gpg_err_set_errno (0); *r_value = strtol (string, &tail, 0); if (errno || !(!strcmp (tail, ".0") || !*tail)) { err = errno? gpg_error_from_errno (errno) : gpg_error (GPG_ERR_BAD_DATA); log_debug ("%s:%d: " "strtol failed for DB returned string (tail=%.10s): %s\n", __FILE__, line, tail, gpg_strerror (err)); *r_value = fallback; } else err = 0; return err; } /* Wrapper around strtoul which prints a warning in case of a * conversion error. On success the converted value is stored at * R_VALUE and 0 is returned; on error FALLBACK is stored at R_VALUE * and an error code is returned. */ static gpg_error_t string_to_ulong (unsigned long *r_value, const char *string, unsigned long fallback, int line) { gpg_error_t err; char *tail = NULL; gpg_err_set_errno (0); *r_value = strtoul (string, &tail, 0); if (errno || !(!strcmp (tail, ".0") || !*tail)) { err = errno? gpg_error_from_errno (errno) : gpg_error (GPG_ERR_BAD_DATA); log_debug ("%s:%d: " "strtoul failed for DB returned string (tail=%.10s): %s\n", __FILE__, line, tail, gpg_strerror (err)); *r_value = fallback; } else err = 0; return err; } /* 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; (void) azColName; log_assert (argc == 1); if (string_to_ulong (count, argv[0], 0, __LINE__)) return 1; /* Abort. */ return 0; } static int get_single_unsigned_long_cb2 (void *cookie, int argc, char **argv, char **azColName, sqlite3_stmt *stmt) { (void) stmt; return get_single_unsigned_long_cb (cookie, argc, argv, azColName); } /* 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 database 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) { char *err = NULL; int rc; unsigned long int count; int version = -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; } /* 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 reading TOFU database: %s\n"), err); print_further_info ("query available tables"); sqlite3_free (err); goto out; } 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); rc = 0; goto out; } else if (rc == SQLITE_ABORT && version == -1) /* Unsupported version. */ { /* An error message was already displayed. */ sqlite3_free (err); goto out; } else if (rc) /* Some error. */ { log_error (_("error determining TOFU database's version: %s\n"), err); sqlite3_free (err); goto out; } else { /* Unexpected success. This can only happen if there are no rows. (select returned 0, but expected ABORT.) */ log_error (_("error determining TOFU database's version: %s\n"), gpg_strerror (GPG_ERR_NO_DATA)); rc = 1; goto out; } } /* 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\n"), err); print_further_info ("create version"); 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\n"), err); print_further_info ("insert version"); sqlite3_free (err); goto out; } /* The list of 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 (TOFU_POLICY_BAD, etc. as an integer). * * 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 occurred, we also set conflict to 0xbaddecaf. */ rc = gpgsql_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); if (rc) { log_error (_("error initializing TOFU database: %s\n"), err); print_further_info ("create bindings"); sqlite3_free (err); goto out; } /* 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\n"), err); print_further_info ("create signatures"); sqlite3_free (err); goto out; } out: if (rc) { rc = sqlite3_exec (db, "rollback;", NULL, NULL, &err); if (rc) { log_error (_("error rolling back transaction on TOFU database: %s\n"), err); sqlite3_free (err); } return 1; } else { rc = sqlite3_exec (db, "end transaction;", NULL, NULL, &err); if (rc) { log_error (_("error committing transaction on TOFU database: %s\n"), err); sqlite3_free (err); return 1; } return 0; } } /* Create a new DB handle. Returns NULL on error. */ /* FIXME: Change to return an error code for better reporting by the caller. */ static tofu_dbs_t opendbs (ctrl_t ctrl) { char *filename; sqlite3 *db; int rc; if (!ctrl->tofu.dbs) { filename = make_filename (gnupg_homedir (), "tofu.db", NULL); rc = sqlite3_open (filename, &db); if (rc) { log_error (_("error opening TOFU database '%s': %s\n"), filename, sqlite3_errmsg (db)); /* Even if an error occurs, DB is guaranteed to be valid. */ sqlite3_close (db); db = NULL; } xfree (filename); /* If a DB is locked wait up to 5 seconds for the lock to be cleared before failing. */ if (db) sqlite3_busy_timeout (db, 5 * 1000); if (db && initdb (db)) { sqlite3_close (db); db = NULL; } if (db) { ctrl->tofu.dbs = xmalloc_clear (sizeof *ctrl->tofu.dbs); ctrl->tofu.dbs->db = db; } } else log_assert (ctrl->tofu.dbs->db); return ctrl->tofu.dbs; } /* Release all of the resources associated with the DB handle. */ void tofu_closedbs (ctrl_t ctrl) { tofu_dbs_t dbs; sqlite3_stmt **statements; dbs = ctrl->tofu.dbs; if (!dbs) return; /* Not initialized. */ log_assert (dbs->in_transaction == 0); end_transaction (ctrl, 2); /* Arghh, that is a surprising use of the struct. */ for (statements = (void *) &dbs->s; (void *) statements < (void *) &(&dbs->s)[1]; statements ++) sqlite3_finalize (*statements); sqlite3_close (dbs->db); xfree (dbs); ctrl->tofu.dbs = NULL; } /* 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; (void) azColName; log_assert (argc == 1); if (string_to_long (count, argv[0], 0, __LINE__)) return 1; /* Abort. */ return 0; } static int get_single_long_cb2 (void *cookie, int argc, char **argv, char **azColName, sqlite3_stmt *stmt) { (void) stmt; return get_single_long_cb (cookie, argc, argv, azColName); } /* 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 (tofu_dbs_t dbs, const char *fingerprint, const char *email, const char *user_id, enum tofu_policy policy, int show_old) { char *fingerprint_pp = format_hexfingerprint (fingerprint, NULL, 0); gpg_error_t rc; char *err = NULL; 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); if (DBG_TRUST || show_old) { /* Get the old policy. Since this is just for informational * purposes, there is no need to start a transaction or to die * if there is a failure. */ /* policy_old needs to be a long and not an enum tofu_policy, because we pass it by reference to get_single_long_cb2, which expects a long. */ long policy_old = TOFU_POLICY_NONE; rc = gpgsql_stepx (dbs->db, &dbs->s.record_binding_get_old_policy, get_single_long_cb2, &policy_old, &err, "select policy from bindings where fingerprint = ? and email = ?", GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_STRING, email, GPGSQL_ARG_END); if (rc) { log_debug ("TOFU: Error reading from binding database" " (reading policy for ): %s\n", fingerprint, email, err); sqlite3_free (err); } if (policy_old != TOFU_POLICY_NONE) (show_old ? log_info : log_debug) ("Changing TOFU trust policy for binding" " from %s to %s.\n", fingerprint, show_old ? user_id : email, tofu_policy_str (policy_old), tofu_policy_str (policy)); else (show_old ? log_info : log_debug) ("Setting TOFU trust policy for new binding" " to %s.\n", fingerprint, show_old ? user_id : email, tofu_policy_str (policy)); if (policy_old == policy) { rc = 0; goto leave; /* Nothing to do. */ } } if (opt.dry_run) { log_info ("TOFU database update skipped due to --dry-run\n"); rc = 0; goto leave; } rc = gpgsql_stepx (dbs->db, &dbs->s.record_binding_update, 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 = ? and email = ?),\n" " ?, ?, ?, strftime('%s','now'), ?);", GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_STRING, email, GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_STRING, email, GPGSQL_ARG_STRING, user_id, GPGSQL_ARG_INT, (int) policy, GPGSQL_ARG_END); if (rc) { log_error (_("error updating TOFU database: %s\n"), err); print_further_info (" insert bindings = %s", fingerprint, email, tofu_policy_str (policy)); sqlite3_free (err); goto leave; } leave: xfree (fingerprint_pp); return rc; } /* 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; } static int strings_collect_cb2 (void *cookie, int argc, char **argv, char **azColName, sqlite3_stmt *stmt) { (void) stmt; return strings_collect_cb (cookie, argc, argv, azColName); } /* 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; /* If the corresponding key/user id has been expired / revoked. */ int is_expired; int is_revoked; /* The key that generated this signature. */ char fingerprint[1]; }; 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_clear (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: . */ static int signature_stats_collect_cb (void *cookie, int argc, char **argv, char **azColName, sqlite3_stmt *stmt) { struct signature_stats **statsp = cookie; int i = 0; enum tofu_policy policy; long time_ago; unsigned long count; long along; (void) azColName; (void) stmt; i ++; if (string_to_long (&along, argv[i], 0, __LINE__)) return 1; /* Abort */ policy = along; i ++; if (! argv[i]) time_ago = 0; else { if (string_to_long (&time_ago, argv[i], 0, __LINE__)) return 1; /* Abort. */ } i ++; /* If time_ago is NULL, then we had no messages, but we still have a single row, which count(*) turns into 1. */ if (! argv[i - 1]) count = 0; else { if (string_to_ulong (&count, argv[i], 0, __LINE__)) return 1; /* Abort */ } i ++; log_assert (argc == i); signature_stats_prepend (statsp, argv[0], policy, time_ago, count); return 0; } /* 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 policy for the binding (email has already been normalized) and any conflict information in *CONFLICT if CONFLICT is not NULL. Returns _tofu_GET_POLICY_ERROR if an error occurs. */ static enum tofu_policy get_policy (tofu_dbs_t dbs, const char *fingerprint, const char *email, char **conflict) { int rc; char *err = NULL; strlist_t strlist = NULL; enum tofu_policy policy = _tofu_GET_POLICY_ERROR; long along; /* Check if the 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 = gpgsql_stepx (dbs->db, &dbs->s.get_policy_select_policy_and_conflict, strings_collect_cb2, &strlist, &err, "select policy, conflict from bindings\n" " where fingerprint = ? and email = ?", GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_STRING, email, GPGSQL_ARG_END); if (rc) { log_error (_("error reading TOFU database: %s\n"), err); print_further_info ("checking for existing bad bindings"); 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 TOFU database: %s\n"), gpg_strerror (GPG_ERR_BAD_DATA)); print_further_info ("checking for existing bad bindings:" " expected 2 results, got %d\n", strlist_length (strlist)); goto out; } /* The result has the right form. */ if (string_to_long (&along, strlist->d, 0, __LINE__)) { log_error (_("error reading TOFU database: %s\n"), gpg_strerror (GPG_ERR_BAD_DATA)); print_further_info ("bad value for policy: %s", strlist->d); goto out; } policy = along; if (! (policy == TOFU_POLICY_AUTO || policy == TOFU_POLICY_GOOD || policy == TOFU_POLICY_UNKNOWN || policy == TOFU_POLICY_BAD || policy == TOFU_POLICY_ASK)) { log_error (_("error reading TOFU database: %s\n"), gpg_strerror (GPG_ERR_DB_CORRUPTED)); print_further_info ("invalid value for policy (%d)", policy); policy = _tofu_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 if 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: log_assert (policy == _tofu_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; } /* Format the first part of a conflict message and return that as a * malloced string. */ static char * format_conflict_msg_part1 (int policy, const char *conflict, const char *fingerprint, const char *email) { estream_t fp; char *binding; int binding_shown = 0; char *tmpstr, *text; binding = xasprintf ("<%s, %s>", fingerprint, email); fp = es_fopenmem (0, "rw,samethread"); if (!fp) log_fatal ("error creating memory stream: %s\n", gpg_strerror (gpg_error_from_syserror())); if (policy == TOFU_POLICY_NONE) { es_fprintf (fp, _("The binding %s is NOT known."), binding); es_fputs (" ", fp); binding_shown = 1; } else if (policy == TOFU_POLICY_ASK /* If there the conflict is with itself, then don't * display this message. */ && conflict && strcmp (conflict, fingerprint)) { es_fprintf (fp, _("The key with fingerprint %s raised a conflict " "with the binding %s." " Since this binding's policy was 'auto', it was " "changed to 'ask'."), conflict, binding); es_fputs (" ", fp); binding_shown = 1; } /* TRANSLATORS: The %s%s is replaced by either a fingerprint and a blank or by two empty strings. */ 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)."), binding_shown ? "" : binding, binding_shown ? "" : " "); es_fputc ('\n', fp); xfree (binding); es_fputc (0, fp); if (es_fclose_snatch (fp, (void **)&tmpstr, NULL)) log_fatal ("error snatching memory stream\n"); text = format_text (tmpstr, 0, 72, 80); es_free (tmpstr); return text; } /* Return 1 if A signed B and B signed A. */ int cross_sigs (kbnode_t a, kbnode_t b) { int i; PKT_public_key *a_pk = a->pkt->pkt.public_key; PKT_public_key *b_pk = b->pkt->pkt.public_key; char a_keyid[33]; char b_keyid[33]; if (DBG_TRUST) { format_keyid (pk_main_keyid (a_pk), KF_DEFAULT, a_keyid, sizeof (a_keyid)); format_keyid (pk_main_keyid (b_pk), KF_DEFAULT, b_keyid, sizeof (b_keyid)); } for (i = 0; i < 2; i ++) { /* See if SIGNER signed SIGNEE. */ kbnode_t signer = i == 0 ? a : b; kbnode_t signee = i == 0 ? b : a; PKT_public_key *signer_pk = signer->pkt->pkt.public_key; u32 *signer_kid = pk_main_keyid (signer_pk); kbnode_t n; /* Iterate over SIGNEE's keyblock and see if there is a valid signature from SIGNER. */ for (n = signee; n; n = n->next) { PKT_signature *sig; if (n->pkt->pkttype != PKT_SIGNATURE) continue; sig = n->pkt->pkt.signature; if (! (sig->sig_class == 0x10 || sig->sig_class == 0x11 || sig->sig_class == 0x12 || sig->sig_class == 0x13)) /* Not a signature over a user id. */ continue; /* SIG is on SIGNEE's keyblock. If SIG was generated by the signer, then it's a match. */ if (keyid_cmp (sig->keyid, signer_kid) == 0) /* Match! */ break; } if (! n) /* We didn't find a signature from signer over signee. */ { if (DBG_TRUST) log_info ("No cross sig between %s and %s\n", a_keyid, b_keyid); return 0; } } /* A signed B and B signed A. */ if (DBG_TRUST) log_info ("Cross sig between %s and %s\n", a_keyid, b_keyid); return 1; } /* 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). * * Note: this function must not be called while in a transaction! */ static void ask_about_binding (ctrl_t ctrl, enum tofu_policy *policy, int *trust_level, int bindings_with_this_email_count, strlist_t bindings_with_this_email, char *conflict, const char *fingerprint, const char *email, const char *user_id) { tofu_dbs_t dbs; char *sqerr = NULL; int rc; estream_t fp; strlist_t other_user_ids = NULL; struct signature_stats *stats = NULL; struct signature_stats *stats_iter = NULL; char *prompt = NULL; char *choices; dbs = ctrl->tofu.dbs; log_assert (dbs); log_assert (dbs->in_transaction == 0); fp = es_fopenmem (0, "rw,samethread"); if (!fp) log_fatal ("error creating memory stream: %s\n", gpg_strerror (gpg_error_from_syserror())); { char *text = format_conflict_msg_part1 (*policy, conflict, fingerprint, email); es_fputs (text, fp); es_fputc ('\n', fp); xfree (text); } begin_transaction (ctrl, 0); /* Find other user ids associated with this key and whether the * bindings are marked as good or bad. */ rc = gpgsql_stepx (dbs->db, &dbs->s.get_trust_gather_other_user_ids, strings_collect_cb2, &other_user_ids, &sqerr, "select user_id, policy from bindings where fingerprint = ?;", GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_END); if (rc) { log_error (_("error gathering other user IDs: %s\n"), sqerr); sqlite3_free (sqerr); sqerr = 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; log_assert (strlist_iter->next); strlist_iter = strlist_iter->next; other_thing = strlist_iter->d; other_policy = atoi (other_thing); es_fprintf (fp, " %s (", other_user_id); es_fprintf (fp, _("policy: %s"), tofu_policy_str (other_policy)); es_fprintf (fp, ")\n"); } es_fprintf (fp, "\n"); free_strlist (other_user_ids); } /* Find other keys associated with this email address. */ /* FIXME: 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 = gpgsql_stepx (dbs->db, &dbs->s.get_trust_gather_other_keys, signature_stats_collect_cb, &stats, &sqerr, "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 < -("STRINGIFY (TIME_AGO_FUTURE_IGNORE)") then -1\n" " when delta < ("STRINGIFY (TIME_AGO_MEDIUM_THRESHOLD)")\n" " then max(0,\n" " round(delta / ("STRINGIFY (TIME_AGO_UNIT_SMALL)"))\n" " * ("STRINGIFY (TIME_AGO_UNIT_SMALL)"))\n" " when delta < ("STRINGIFY (TIME_AGO_LARGE_THRESHOLD)")\n" " then round(delta / ("STRINGIFY (TIME_AGO_UNIT_MEDIUM)"))\n" " * ("STRINGIFY (TIME_AGO_UNIT_MEDIUM)")\n" " else round(delta / ("STRINGIFY (TIME_AGO_UNIT_LARGE)"))\n" " * ("STRINGIFY (TIME_AGO_UNIT_LARGE)")\n" " end time_ago,\n" " delta time_ago_raw\n" " from bindings\n" " left join\n" " (select *,\n" " cast(strftime('%s','now') - sig_time as real) delta\n" " from signatures) ss\n" " on ss.binding = bindings.oid)\n" " where email = ?\n" " group by fingerprint, time_ago\n" /* Make sure the current key is first. */ " order by fingerprint = ? asc, fingerprint desc, time_ago desc;\n", GPGSQL_ARG_STRING, email, GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_END); if (rc) { strlist_t strlist_iter; log_error (_("error gathering signature stats: %s\n"), sqerr); sqlite3_free (sqerr); sqerr = NULL; es_fprintf (fp, ngettext("The email address \"%s\" is" " associated with %d key:\n", "The email address \"%s\" is" " associated with %d keys:\n", bindings_with_this_email_count), 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 { int stats_count = 0; kbnode_t *kb_all; KEYDB_HANDLE hd; int i; char *key = NULL; /* Get the keyblock for each key. */ for (stats_iter = stats; stats_iter; stats_iter = stats_iter->next) stats_count ++; kb_all = xcalloc (sizeof (kb_all[0]), stats_count); if (! stats || strcmp (stats->fingerprint, fingerprint)) { /* 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); } /* Figure out which user ids are revoked or expired. */ hd = keydb_new (); for (stats_iter = stats, i = 0; stats_iter; stats_iter = stats_iter->next, i ++) { KEYDB_SEARCH_DESC desc; kbnode_t kb; PKT_public_key *pk; kbnode_t n; int found_user_id; rc = keydb_search_reset (hd); if (rc) { log_error (_("resetting keydb: %s\n"), gpg_strerror (rc)); continue; } rc = classify_user_id (stats_iter->fingerprint, &desc, 0); if (rc) { log_error (_("error parsing key specification '%s': %s\n"), stats_iter->fingerprint, gpg_strerror (rc)); continue; } rc = keydb_search (hd, &desc, 1, NULL); if (rc) { log_error (_("key \"%s\" not found: %s\n"), stats_iter->fingerprint, gpg_strerror (rc)); continue; } rc = keydb_get_keyblock (hd, &kb); if (rc) { log_error (_("error reading keyblock: %s\n"), gpg_strerror (rc)); print_further_info ("fingerprint: %s", stats_iter->fingerprint); continue; } merge_keys_and_selfsig (kb); log_assert (kb->pkt->pkttype == PKT_PUBLIC_KEY); kb_all[i] = kb; pk = kb->pkt->pkt.public_key; if (pk->has_expired) stats_iter->is_expired = 1; if (pk->flags.revoked) stats_iter->is_revoked = 1; n = kb; found_user_id = 0; while ((n = find_next_kbnode (n, PKT_USER_ID)) && ! found_user_id) { PKT_user_id *user_id2 = n->pkt->pkt.user_id; char *email2; if (user_id2->attrib_data) continue; email2 = email_from_user_id (user_id2->name); if (strcmp (email, email2) == 0) { found_user_id = 1; if (user_id2->is_revoked) stats_iter->is_revoked = 1; if (user_id2->is_expired) stats_iter->is_expired = 1; } xfree (email2); } if (! found_user_id) log_info (_("TOFU db may be corrupted: user id (%s)" " not on key block (%s)\n"), email, fingerprint); } keydb_release (hd); { int j; struct signature_stats **stats_prevp; struct signature_stats *stats_iter_next; int die[stats_count]; memset (die, 0, sizeof (die)); for (i = 0; i < stats_count; i ++) { /* i or a key that has cross sigs with i (possible indirectly)? */ if (! (i == 0 || die[i])) continue; for (j = i + 1; j < stats_count; j ++) if (cross_sigs (kb_all[i], kb_all[j])) die[j] = 1; } /* Free the dead stat structures. */ for (stats_iter = stats, stats_prevp = &stats, i = 0; stats_iter; stats_iter = stats_iter_next, i ++) { stats_iter_next = stats_iter->next; release_kbnode (kb_all[i]); if (die[i]) { *stats_prevp = stats_iter_next; stats_iter->next = NULL; signature_stats_free (stats_iter); bindings_with_this_email_count --; } else { stats_prevp = &stats_iter->next; } } } log_assert (stats); log_assert (bindings_with_this_email_count >= 1); if ((*policy == TOFU_POLICY_NONE && bindings_with_this_email_count == 1) || (*policy == TOFU_POLICY_ASK && conflict)) if (bindings_with_this_email_count == 1) { /* All "conflicts" were not really conflicts. */ log_assert (! stats->next); if (DBG_TRUST) log_debug ("%s: all apparent TOFU conflicts are legitimate " "(cross sigs), setting policy to auto.\n", stats_iter->fingerprint); *policy = TOFU_POLICY_AUTO; record_binding (dbs, fingerprint, email, user_id, *policy, 0); *trust_level = tofu_policy_to_trust_level (*policy); goto out; } es_fprintf (fp, _("Statistics for potentially conflicting keys" " with the email address \"%s\":\n"), email); for (stats_iter = stats; stats_iter; stats_iter = stats_iter->next) { if (! key || strcmp (key, stats_iter->fingerprint)) { int this_key; char *key_pp; key = stats_iter->fingerprint; this_key = strcmp (key, fingerprint) == 0; key_pp = format_hexfingerprint (key, NULL, 0); es_fprintf (fp, " %s (", key_pp); if (stats_iter->is_revoked) { es_fprintf (fp, _("revoked")); es_fprintf (fp, _(", ")); } else if (stats_iter->is_expired) { es_fprintf (fp, _("expired")); es_fprintf (fp, _(", ")); } if (this_key) es_fprintf (fp, _("this key")); else es_fprintf (fp, _("policy: %s"), tofu_policy_str (stats_iter->policy)); es_fputs ("):\n", fp); xfree (key_pp); } es_fputs (" ", fp); if (stats_iter->time_ago == -1) es_fprintf (fp, ngettext("%ld message signed in the future.", "%ld messages signed in the future.", stats_iter->count), stats_iter->count); else { long t_scaled = time_ago_scale (stats_iter->time_ago); /* TANSLATORS: This string is concatenated with one of * the day/week/month strings to form one sentence. */ es_fprintf (fp, ngettext("%ld message signed", "%ld messages signed", stats_iter->count), stats_iter->count); if (!stats_iter->count) es_fputs (".", fp); else if (stats_iter->time_ago < TIME_AGO_UNIT_MEDIUM) es_fprintf (fp, ngettext(" over the past %ld day.", " over the past %ld days.", t_scaled), t_scaled); else if (stats_iter->time_ago < TIME_AGO_UNIT_LARGE) es_fprintf (fp, ngettext(" over the past %ld week.", " over the past %ld weeks.", t_scaled), t_scaled); else es_fprintf (fp, ngettext(" over the past %ld month.", " over the past %ld months.", t_scaled), t_scaled); } es_fputs ("\n", fp); } } end_transaction (ctrl, 0); if ((*policy == TOFU_POLICY_NONE && bindings_with_this_email_count > 0) || (*policy == TOFU_POLICY_ASK && (conflict || bindings_with_this_email_count > 0))) { /* This is a conflict. */ /* TRANSLATORS: Please translate the text found in the source * file below. We don't directly internationalize that text so * that we can tweak it without breaking translations. */ char *text = _("TOFU detected a binding conflict"); char *textbuf; if (!strcmp (text, "TOFU detected a binding conflict")) { /* 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."; } textbuf = format_text (text, 0, 72, 80); es_fprintf (fp, "\n%s\n", textbuf); xfree (textbuf); } es_fputc ('\n', fp); /* Add a NUL terminator. */ es_fputc (0, fp); if (es_fclose_snatch (fp, (void **) &prompt, NULL)) log_fatal ("error snatching memory stream\n"); /* I think showing the large message once is sufficient. If we * would move it right before the cpr_get many lines will scroll * away and the user might not realize that he merely entered a * wrong choise (because he does not see that either). As a small * benefit we allow C-L to redisplay everything. */ tty_printf ("%s", prompt); /* Suspend any transaction: it could take a while until the user responds. */ tofu_suspend_batch_transaction (ctrl); while (1) { char *response; /* 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"); if (strlen (choices) != 10) log_bug ("Bad TOFU conflict translation! Please report."); response = cpr_get ("tofu.conflict", _("(G)ood, (A)ccept once, (U)nknown, (R)eject once, (B)ad? ")); trim_spaces (response); cpr_kill_prompt (); if (*response == CONTROL_L) tty_printf ("%s", prompt); else if (!response[0]) /* Default to unknown. Don't save it. */ { tty_printf (_("Defaulting to unknown.")); *policy = TOFU_POLICY_UNKNOWN; break; } else if (!response[1]) { char *choice = strchr (choices, *response); if (choice) { int c = ((size_t) choice - (size_t) choices) / 2; 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)) { /* If there's an error registering the * binding, don't save the signature. */ *trust_level = _tofu_GET_TRUST_ERROR; } break; } } xfree (response); } out: tofu_resume_batch_transaction (ctrl); xfree (prompt); signature_stats_free (stats); } /* Return the trust level (TRUST_NEVER, etc.) for the binding * (email is already normalized). If no policy * is registered, returns TOFU_POLICY_NONE. If an error occurs, * returns _tofu_GET_TRUST_ERROR. * * PK is the public key object for FINGERPRINT. * * USER_ID is the unadulterated 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. Note: if MAY_ASK is set, then this * function must not be called while in a transaction! */ static enum tofu_policy get_trust (ctrl_t ctrl, PKT_public_key *pk, const char *fingerprint, const char *email, const char *user_id, int may_ask) { tofu_dbs_t dbs = ctrl->tofu.dbs; int in_transaction = 0; enum tofu_policy policy; char *conflict = NULL; int rc; char *sqerr = 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; log_assert (dbs); if (may_ask) log_assert (dbs->in_transaction == 0); if (opt.batch) may_ask = 0; /* Make sure _tofu_GET_TRUST_ERROR isn't equal to any of the trust levels. */ log_assert (_tofu_GET_TRUST_ERROR != TRUST_UNKNOWN && _tofu_GET_TRUST_ERROR != TRUST_EXPIRED && _tofu_GET_TRUST_ERROR != TRUST_UNDEFINED && _tofu_GET_TRUST_ERROR != TRUST_NEVER && _tofu_GET_TRUST_ERROR != TRUST_MARGINAL && _tofu_GET_TRUST_ERROR != TRUST_FULLY && _tofu_GET_TRUST_ERROR != TRUST_ULTIMATE); begin_transaction (ctrl, 0); in_transaction = 1; policy = get_policy (dbs, fingerprint, email, &conflict); { /* See if the key is ultimately trusted. If so, we're done. */ u32 kid[2]; keyid_from_pk (pk, kid); if (tdb_keyid_is_utk (kid)) { if (policy == TOFU_POLICY_NONE) { if (record_binding (dbs, fingerprint, email, user_id, TOFU_POLICY_AUTO, 0) != 0) { log_error (_("error setting TOFU binding's trust level" " to %s\n"), "auto"); trust_level = _tofu_GET_TRUST_ERROR; goto out; } } trust_level = TRUST_ULTIMATE; goto out; } } if (policy == TOFU_POLICY_AUTO) { policy = opt.tofu_default_policy; if (DBG_TRUST) log_debug ("TOFU: binding '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 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 _tofu_GET_POLICY_ERROR: trust_level = _tofu_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 needed 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 = gpgsql_stepx (dbs->db, &dbs->s.get_trust_bindings_with_this_email, strings_collect_cb2, &bindings_with_this_email, &sqerr, "select distinct fingerprint from bindings where email = ?;", GPGSQL_ARG_STRING, email, GPGSQL_ARG_END); if (rc) { log_error (_("error reading TOFU database: %s\n"), sqerr); print_further_info ("listing fingerprints"); sqlite3_free (sqerr); 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. */ log_assert (policy == TOFU_POLICY_NONE); if (DBG_TRUST) log_debug ("TOFU: New binding , no conflict.\n", fingerprint, email); if (record_binding (dbs, fingerprint, email, user_id, TOFU_POLICY_AUTO, 0) != 0) { log_error (_("error setting TOFU binding's trust level to %s\n"), "auto"); trust_level = _tofu_GET_TRUST_ERROR; goto out; } trust_level = 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). */ log_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; } /* We can't be in a normal transaction in ask_about_binding. */ end_transaction (ctrl, 0); in_transaction = 0; /* If we get here, we need to ask the user about the binding. */ ask_about_binding (ctrl, &policy, &trust_level, bindings_with_this_email_count, bindings_with_this_email, conflict, fingerprint, email, user_id); out: if (in_transaction) end_transaction (ctrl, 0); if (change_conflicting_to_ask) { if (! may_ask) { /* If we weren't allowed to ask, also update this key as conflicting with itself. */ rc = gpgsql_exec_printf (dbs->db, NULL, NULL, &sqerr, "update bindings set policy = %d, conflict = %Q" " where email = %Q" " and (policy = %d or (policy = %d and fingerprint = %Q));", TOFU_POLICY_ASK, fingerprint, email, TOFU_POLICY_AUTO, TOFU_POLICY_ASK, fingerprint); } else { rc = gpgsql_exec_printf (dbs->db, NULL, NULL, &sqerr, "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"), sqerr); sqlite3_free (sqerr); sqerr = NULL; } } xfree (conflict); free_strlist (bindings_with_this_email); return trust_level; } /* Return a malloced string of the form * "7 months, 1 day, 5 minutes, 0 seconds" * The caller should replace all '~' in the returned string by a space * and also free the returned string. * * This is actually a bad hack which may not work correctly with all * languages. */ static char * time_ago_str (long long int t) { estream_t fp; 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; char *str; /* 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 (t > YEAR_SECS) { years = t / YEAR_SECS; t -= years * YEAR_SECS; } if (t > MONTH_SECS) { months = t / MONTH_SECS; t -= months * MONTH_SECS; } if (t > DAY_SECS) { days = t / DAY_SECS; t -= days * DAY_SECS; } if (t > HOUR_SECS) { hours = t / HOUR_SECS; t -= hours * HOUR_SECS; } if (t > MIN_SECS) { minutes = t / MIN_SECS; t -= minutes * MIN_SECS; } seconds = t; #undef MIN_SECS #undef HOUR_SECS #undef DAY_SECS #undef MONTH_SECS #undef YEAR_SECS fp = es_fopenmem (0, "rw,samethread"); if (! fp) log_fatal ("error creating memory stream: %s\n", gpg_strerror (gpg_error_from_syserror())); if (years) { /* TRANSLATORS: The tilde ('~') is used here to indicate a * non-breakable space */ es_fprintf (fp, ngettext("%d~year", "%d~years", years), years); count ++; first = i; } i ++; if ((first == -1 || i - first <= 3) && months) { if (count) es_fprintf (fp, ", "); es_fprintf (fp, ngettext("%d~month", "%d~months", months), months); count ++; first = i; } i ++; if ((first == -1 || i - first <= 3) && count < 2 && days) { if (count) es_fprintf (fp, ", "); es_fprintf (fp, ngettext("%d~day", "%d~days", days), days); count ++; first = i; } i ++; if ((first == -1 || i - first <= 3) && count < 2 && hours) { if (count) es_fprintf (fp, ", "); es_fprintf (fp, ngettext("%d~hour", "%d~hours", hours), hours); count ++; first = i; } i ++; if ((first == -1 || i - first <= 3) && count < 2 && minutes) { if (count) es_fprintf (fp, ", "); es_fprintf (fp, ngettext("%d~minute", "%d~minutes", minutes), minutes); count ++; first = i; } i ++; if ((first == -1 || i - first <= 3) && count < 2) { if (count) es_fprintf (fp, ", "); es_fprintf (fp, ngettext("%d~second", "%d~seconds", seconds), seconds); } es_fputc (0, fp); if (es_fclose_snatch (fp, (void **) &str, NULL)) log_fatal ("error snatching memory stream\n"); return str; } /* If FP is NULL, write TOFU_STATS status line. If FP is not NULL * write a "tfs" record to that stream. */ static void write_stats_status (estream_t fp, long messages, enum tofu_policy policy, unsigned long first_seen, unsigned long most_recent_seen) { const char *validity; if (messages < 1) validity = "1"; /* Key without history. */ else if (messages < BASIC_TRUST_THRESHOLD) validity = "2"; /* Key with too little history. */ else if (messages < FULL_TRUST_THRESHOLD) validity = "3"; /* Key with enough history for basic trust. */ else validity = "4"; /* Key with a lot of history. */ if (fp) { es_fprintf (fp, "tfs:1:%s:%ld:0:%s:%lu:%lu:\n", validity, messages, tofu_policy_str (policy), first_seen, most_recent_seen); } else { char numbuf1[35]; char numbuf2[35]; char numbuf3[35]; snprintf (numbuf1, sizeof numbuf1, " %ld", messages); *numbuf2 = *numbuf3 = 0; if (first_seen && most_recent_seen) { snprintf (numbuf2, sizeof numbuf2, " %lu", first_seen); snprintf (numbuf3, sizeof numbuf3, " %lu", most_recent_seen); } write_status_strings (STATUS_TOFU_STATS, validity, numbuf1, " 0", " ", tofu_policy_str (policy), numbuf2, numbuf3, NULL); } } /* Note: If OUTFP is not NULL, this function merely prints a "tfs" record * to OUTFP. In this case USER_ID is not required. */ static void show_statistics (tofu_dbs_t dbs, const char *fingerprint, const char *email, const char *user_id, estream_t outfp) { char *fingerprint_pp; int rc; strlist_t strlist = NULL; char *err = NULL; fingerprint_pp = format_hexfingerprint (fingerprint, NULL, 0); rc = gpgsql_exec_printf (dbs->db, strings_collect_cb, &strlist, &err, "select count (*), min (signatures.time), max (signatures.time)\n" " from signatures\n" " left join bindings on signatures.binding = bindings.oid\n" " where fingerprint = %Q and email = %Q;", fingerprint, email); if (rc) { log_error (_("error reading TOFU database: %s\n"), err); print_further_info ("getting statistics"); sqlite3_free (err); goto out; } if (!outfp) write_status_text_and_buffer (STATUS_TOFU_USER, fingerprint, email, strlen (email), 0); if (! strlist) { if (!outfp) log_info (_("Have never verified a message signed by key %s!\n"), fingerprint_pp); write_stats_status (outfp, 0, TOFU_POLICY_NONE, 0, 0); } else { unsigned long now = gnupg_get_time (); signed long messages; unsigned long first_seen; unsigned long most_recent_seen; log_assert (strlist_length (strlist) == 3); string_to_long (&messages, strlist->d, -1, __LINE__); if (messages == 0 && *strlist->next->d == '\0') { /* min(NULL) => NULL => "". */ first_seen = 0; most_recent_seen = 0; } else { string_to_ulong (&first_seen, strlist->next->d, -1, __LINE__); if (first_seen > now) { log_debug ("time-warp - tofu DB has a future value (%lu, %lu)\n", first_seen, now); first_seen = now; } string_to_ulong (&most_recent_seen, strlist->next->next->d, -1, __LINE__); if (most_recent_seen > now) { log_debug ("time-warp - tofu DB has a future value (%lu, %lu)\n", most_recent_seen, now); most_recent_seen = now; } } if (messages == -1 || first_seen == -1) { write_stats_status (outfp, 0, TOFU_POLICY_NONE, 0, 0); if (!outfp) log_info (_("Failed to collect signature statistics for \"%s\"\n" "(key %s)\n"), user_id, fingerprint_pp); } else if (outfp) { write_stats_status (outfp, messages, get_policy (dbs, fingerprint, email, NULL), first_seen, most_recent_seen); } else { enum tofu_policy policy = get_policy (dbs, fingerprint, email, NULL); estream_t fp; char *msg; write_stats_status (NULL, messages, policy, first_seen, most_recent_seen); fp = es_fopenmem (0, "rw,samethread"); if (! fp) log_fatal ("error creating memory stream: %s\n", gpg_strerror (gpg_error_from_syserror())); if (messages == 0) { es_fprintf (fp, _("Verified %ld messages signed by \"%s\"."), 0L, user_id); es_fputc ('\n', fp); } else { char *first_seen_ago_str = time_ago_str (now - first_seen); /* TRANSLATORS: The final %s is replaced by a string like "7 months, 1 day, 5 minutes, 0 seconds". */ es_fprintf (fp, ngettext("Verified %ld message signed by \"%s\"\n" "in the past %s.", "Verified %ld messages signed by \"%s\"\n" "in the past %s.", messages), messages, user_id, first_seen_ago_str); if (messages > 1) { char *tmpstr = time_ago_str (now - most_recent_seen); es_fputs (" ", fp); es_fprintf (fp, _("The most recent message was" " verified %s ago."), tmpstr); xfree (tmpstr); } xfree (first_seen_ago_str); if (opt.verbose) { es_fputs (" ", fp); es_fputc ('(', fp); es_fprintf (fp, _("policy: %s"), tofu_policy_str (policy)); es_fputs (")\n", fp); } else es_fputs ("\n", fp); } { char *tmpmsg, *p; es_fputc (0, fp); if (es_fclose_snatch (fp, (void **) &tmpmsg, NULL)) log_fatal ("error snatching memory stream\n"); msg = format_text (tmpmsg, 0, 72, 80); es_free (tmpmsg); /* Print a status line but suppress the trailing LF. * Spaces are not percent escaped. */ if (*msg) write_status_buffer (STATUS_TOFU_STATS_LONG, msg, strlen (msg)-1, -1); /* Remove the non-breaking space markers. */ for (p=msg; *p; p++) if (*p == '~') *p = ' '; } log_string (GPGRT_LOG_INFO, msg); xfree (msg); if (policy == TOFU_POLICY_AUTO && messages < BASIC_TRUST_THRESHOLD) { char *set_policy_command; char *text; char *tmpmsg; if (messages == 0) log_info (_("Warning: we have yet to see" " a message signed by this key and user id!\n")); else if (messages == 1) log_info (_("Warning: we've only seen a single message" " signed by this key and user id!\n")); set_policy_command = xasprintf ("gpg --tofu-policy bad %s", fingerprint); tmpmsg = xasprintf (ngettext ("Warning: if you think you've seen more than %ld message " "signed by this key and user id, then this key might be a " "forgery! Carefully examine the email address for small " "variations. If the key is suspect, then use\n" " %s\n" "to mark it as being bad.\n", "Warning: if you think you've seen more than %ld messages " "signed by this key, then this key might be a forgery! " "Carefully examine the email address for small " "variations. If the key is suspect, then use\n" " %s\n" "to mark it as being bad.\n", messages), messages, set_policy_command); text = format_text (tmpmsg, 0, 72, 80); xfree (tmpmsg); log_string (GPGRT_LOG_INFO, text); xfree (text); es_free (set_policy_command); } } } out: free_strlist (strlist); xfree (fingerprint_pp); 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 or we are out of core. 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; } /* Register the signature with the bindings , for each USER_ID in USER_ID_LIST. The fingerprint is taken from the primary key packet PK. SIG_DIGEST_BIN is the binary representation of the message's digest. SIG_DIGEST_BIN_LEN is its length. 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 0 on success and an error code if an error occured. */ gpg_error_t tofu_register (ctrl_t ctrl, PKT_public_key *pk, strlist_t user_id_list, const byte *sig_digest_bin, int sig_digest_bin_len, time_t sig_time, const char *origin) { gpg_error_t rc; tofu_dbs_t dbs; char *fingerprint = NULL; strlist_t user_id; char *email = NULL; char *err = NULL; char *sig_digest; unsigned long c; dbs = opendbs (ctrl); if (! dbs) { rc = gpg_error (GPG_ERR_GENERAL); log_error (_("error opening TOFU database: %s\n"), gpg_strerror (rc)); return rc; } /* We do a query and then an insert. Make sure they are atomic by wrapping them in a transaction. */ rc = begin_transaction (ctrl, 0); if (rc) return rc; sig_digest = make_radix64_string (sig_digest_bin, sig_digest_bin_len); fingerprint = hexfingerprint (pk, NULL, 0); if (! origin) /* The default origin is simply "unknown". */ origin = "unknown"; for (user_id = user_id_list; user_id; user_id = user_id->next) { email = email_from_user_id (user_id->d); if (DBG_TRUST) log_debug ("TOFU: Registering signature %s with binding" " \n", sig_digest, fingerprint, email); /* Make sure the binding exists and record any TOFU conflicts. */ if (get_trust (ctrl, pk, fingerprint, email, user_id->d, 0) == _tofu_GET_TRUST_ERROR) { rc = gpg_error (GPG_ERR_GENERAL); xfree (email); break; } /* If we've already seen this signature before, then don't add it again. */ rc = gpgsql_stepx (dbs->db, &dbs->s.register_already_seen, get_single_unsigned_long_cb2, &c, &err, "select count (*)\n" " from signatures left join bindings\n" " on signatures.binding = bindings.oid\n" " where fingerprint = ? and email = ? and sig_time = ?\n" " and sig_digest = ?", GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_STRING, email, GPGSQL_ARG_LONG_LONG, (long long) sig_time, GPGSQL_ARG_STRING, sig_digest, GPGSQL_ARG_END); if (rc) { log_error (_("error reading TOFU database: %s\n"), err); print_further_info ("checking existence"); sqlite3_free (err); } else if (c > 1) /* Duplicates! This should not happen. In particular, because is the primary key! */ log_debug ("SIGNATURES DB contains duplicate records" " ." " Please report.\n", fingerprint, email, (unsigned long) sig_time, sig_digest, origin); else if (c == 1) { if (DBG_TRUST) log_debug ("Already observed the signature and binding" " \n", fingerprint, email, (unsigned long) sig_time, sig_digest, origin); } else if (opt.dry_run) { log_info ("TOFU database update skipped due to --dry-run\n"); } else /* This is the first time that we've seen this signature and binding. Record it. */ { if (DBG_TRUST) log_debug ("TOFU: Saving signature" " \n", fingerprint, email, sig_digest); log_assert (c == 0); rc = gpgsql_stepx (dbs->db, &dbs->s.register_insert, NULL, NULL, &err, "insert into signatures\n" " (binding, sig_digest, origin, sig_time, time)\n" " values\n" " ((select oid from bindings\n" " where fingerprint = ? and email = ?),\n" " ?, ?, ?, strftime('%s', 'now'));", GPGSQL_ARG_STRING, fingerprint, GPGSQL_ARG_STRING, email, GPGSQL_ARG_STRING, sig_digest, GPGSQL_ARG_STRING, origin, GPGSQL_ARG_LONG_LONG, (long long) sig_time, GPGSQL_ARG_END); if (rc) { log_error (_("error updating TOFU database: %s\n"), err); print_further_info ("insert signatures"); sqlite3_free (err); } } xfree (email); if (rc) break; } if (rc) rollback_transaction (ctrl); else rc = end_transaction (ctrl, 0); xfree (fingerprint); xfree (sig_digest); return rc; } /* 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); log_assert (tofu == TRUST_UNKNOWN || tofu == TRUST_EXPIRED || tofu == TRUST_UNDEFINED || tofu == TRUST_NEVER || tofu == TRUST_MARGINAL || tofu == TRUST_FULLY || tofu == TRUST_ULTIMATE); log_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) return upper | TRUST_ULTIMATE | TRUST_FLAG_TOFU_BASED; if (wot == TRUST_ULTIMATE) return upper | TRUST_ULTIMATE; if (tofu == TRUST_FULLY) return upper | TRUST_FULLY | TRUST_FLAG_TOFU_BASED; if (wot == TRUST_FULLY) return upper | TRUST_FULLY; if (tofu == TRUST_MARGINAL) return upper | TRUST_MARGINAL | TRUST_FLAG_TOFU_BASED; if (wot == TRUST_MARGINAL) return upper | TRUST_MARGINAL; if (tofu == TRUST_UNDEFINED) return upper | TRUST_UNDEFINED | TRUST_FLAG_TOFU_BASED; if (wot == TRUST_UNDEFINED) return upper | TRUST_UNDEFINED; return upper | TRUST_UNKNOWN; } /* Write a "tfs" record for a --with-colons listing. */ gpg_error_t tofu_write_tfs_record (ctrl_t ctrl, estream_t fp, PKT_public_key *pk, const char *user_id) { gpg_error_t err; tofu_dbs_t dbs; char *fingerprint; char *email; if (!*user_id) return 0; /* No TOFU stats possible for an empty ID. */ dbs = opendbs (ctrl); if (!dbs) { err = gpg_error (GPG_ERR_GENERAL); log_error (_("error opening TOFU database: %s\n"), gpg_strerror (err)); return err; } fingerprint = hexfingerprint (pk, NULL, 0); email = email_from_user_id (user_id); show_statistics (dbs, fingerprint, email, user_id, fp); xfree (email); xfree (fingerprint); return 0; } /* Return the validity (TRUST_NEVER, etc.) of the bindings , for each USER_ID in USER_ID_LIST. If USER_ID_LIST->FLAG is set, then the id is considered to be expired. PK is the primary key packet. If MAY_ASK is 1 and the policy is TOFU_POLICY_ASK, then the user will be prompted to choose a policy. If MAY_ASK is 0 and the policy is TOFU_POLICY_ASK, then TRUST_UNKNOWN is returned. Returns TRUST_UNDEFINED if an error occurs. */ int tofu_get_validity (ctrl_t ctrl, PKT_public_key *pk, strlist_t user_id_list, int may_ask) { tofu_dbs_t dbs; char *fingerprint = NULL; strlist_t user_id; int trust_level = TRUST_UNKNOWN; int bindings = 0; int bindings_valid = 0; dbs = opendbs (ctrl); if (! dbs) { log_error (_("error opening TOFU database: %s\n"), gpg_strerror (GPG_ERR_GENERAL)); return TRUST_UNDEFINED; } fingerprint = hexfingerprint (pk, NULL, 0); tofu_begin_batch_update (ctrl); tofu_resume_batch_transaction (ctrl); for (user_id = user_id_list; user_id; user_id = user_id->next, bindings ++) { char *email = email_from_user_id (user_id->d); /* Always call get_trust to make sure the binding is registered. */ int tl = get_trust (ctrl, pk, fingerprint, email, user_id->d, may_ask); if (tl == _tofu_GET_TRUST_ERROR) { /* An error. */ trust_level = TRUST_UNDEFINED; xfree (email); goto die; } if (DBG_TRUST) log_debug ("TOFU: validity for : %s%s.\n", fingerprint, email, trust_value_to_string (tl), user_id->flags ? " (but expired)" : ""); if (user_id->flags) tl = TRUST_EXPIRED; if (tl != TRUST_EXPIRED) bindings_valid ++; if (may_ask && tl != TRUST_ULTIMATE && tl != TRUST_EXPIRED) show_statistics (dbs, fingerprint, email, user_id->d, NULL); if (tl == TRUST_NEVER) trust_level = TRUST_NEVER; else if (tl == TRUST_EXPIRED) /* Ignore expired bindings in the trust calculation. */ ; else if (tl > trust_level) { /* The expected values: */ log_assert (tl == TRUST_UNKNOWN || tl == TRUST_UNDEFINED || tl == TRUST_MARGINAL || tl == TRUST_FULLY || tl == TRUST_ULTIMATE); /* We assume the following ordering: */ log_assert (TRUST_UNKNOWN < TRUST_UNDEFINED); log_assert (TRUST_UNDEFINED < TRUST_MARGINAL); log_assert (TRUST_MARGINAL < TRUST_FULLY); log_assert (TRUST_FULLY < TRUST_ULTIMATE); trust_level = tl; } xfree (email); } die: tofu_end_batch_update (ctrl); xfree (fingerprint); if (bindings_valid == 0) { if (DBG_TRUST) log_debug ("no (of %d) valid bindings." " Can't get TOFU validity for this set of user ids.\n", bindings); return TRUST_NEVER; } return trust_level; } /* 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 (ctrl_t ctrl, kbnode_t kb, enum tofu_policy policy) { tofu_dbs_t dbs; PKT_public_key *pk; char *fingerprint = NULL; log_assert (kb->pkt->pkttype == PKT_PUBLIC_KEY); pk = kb->pkt->pkt.public_key; dbs = opendbs (ctrl); if (! dbs) { log_error (_("error opening TOFU database: %s\n"), gpg_strerror (GPG_ERR_GENERAL)); 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 = hexfingerprint (pk, NULL, 0); begin_transaction (ctrl, 0); 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); } end_transaction (ctrl, 0); xfree (fingerprint); 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 (ctrl_t ctrl, 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 (ctrl, 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 (ctrl_t ctrl, PKT_public_key *pk, PKT_user_id *user_id, enum tofu_policy *policy) { tofu_dbs_t dbs; char *fingerprint; char *email; /* Make sure PK is a primary key. */ log_assert (pk->main_keyid[0] == pk->keyid[0] && pk->main_keyid[1] == pk->keyid[1]); dbs = opendbs (ctrl); if (! dbs) { log_error (_("error opening TOFU database: %s\n"), gpg_strerror (GPG_ERR_GENERAL)); return gpg_error (GPG_ERR_GENERAL); } fingerprint = hexfingerprint (pk, NULL, 0); email = email_from_user_id (user_id->name); *policy = get_policy (dbs, fingerprint, email, NULL); xfree (email); xfree (fingerprint); if (*policy == _tofu_GET_POLICY_ERROR) return gpg_error (GPG_ERR_GENERAL); return 0; }