[pacman-dev,2/5,v3] signing: add ability to import keys using a WKD

Message ID 20190805103729.7328-1-diabonas@gmx.de
State New
Headers show
Series
  • [pacman-dev,v2] signing: add ability to import keys using a WKD
Related show

Commit Message

Jonas Witschel Aug. 5, 2019, 10:37 a.m. UTC
Currently pacman relies on the SKS keyserver network to fetch unknown
PGP keys. These keyservers are vulnerable to signature spamming attacks,
potentionally making it impossible to import the required keys. An
alternative to keyservers is a so-called Web Key Directory (WKD), a
well-known, trusted location on a server from where the keys can be
fetched.

This commit adds the ability to retrieve keys from the WKD. Due to the
mentioned vulnerabilities, the WKD is tried first, falling back to the
keyservers only if no appropriate key is found there.

In contrast to keyservers, keys on the WKD are not looked up using their
fingerprint, but by email address. Since the email address of the
signing key is usually not included in the signature, we will use the
email packager email address. In contrast to the direct fingerprint
lookup, this might not work or result in a different key, so we check
the fingerprint of the received key and fall back to a keyserver if
necessary. The extraction of the email address is not performed here,
this will be included in the subsequent commits.

GnuPG/GPGME handles keys from a WKD very differently than keys from a
keyserver: gpgme_get_key directly imports the key into the keyring if it
is available without giving the user the choice to confirm it first. On
the other hand gpgme_op_import_keys does not import keys from a WKD
because it is hard-coded to use keyservers (gpg --recv-keys). Therefore
we set a temporary keyring for the initial lookup and use gpgme_get_key
after the user has confirmed he wants to import the key. This makes it
necessary to add a new member "location" to _alpm_pgpkey_t in order to
distinguish the two methods to retrieve the key.

Also see FS#63171.

Signed-off-by: Jonas Witschel <diabonas@gmx.de>
---
 lib/libalpm/alpm.h       |   1 +
 lib/libalpm/be_package.c |   2 +-
 lib/libalpm/signing.c    | 184 +++++++++++++++++++++++++++++----------
 lib/libalpm/signing.h    |   2 +-
 lib/libalpm/sync.c       |   2 +-
 5 files changed, 140 insertions(+), 51 deletions(-)

--
2.22.0

Patch

diff --git a/lib/libalpm/alpm.h b/lib/libalpm/alpm.h
index ffb2ad96..e010676d 100644
--- a/lib/libalpm/alpm.h
+++ b/lib/libalpm/alpm.h
@@ -300,6 +300,7 @@  typedef struct _alpm_pgpkey_t {
 	unsigned int length;
 	unsigned int revoked;
 	char pubkey_algo;
+	char location;
 } alpm_pgpkey_t;

 /**
diff --git a/lib/libalpm/be_package.c b/lib/libalpm/be_package.c
index ac911bdb..fbb0d43e 100644
--- a/lib/libalpm/be_package.c
+++ b/lib/libalpm/be_package.c
@@ -755,7 +755,7 @@  int SYMEXPORT alpm_pkg_load(alpm_handle_t *handle, const char *filename, int ful
 				for(k = keys; k; k = k->next) {
 					char *key = k->data;
 					if(_alpm_key_in_keychain(handle, key) == 0) {
-						if(_alpm_key_import(handle, key) == -1) {
+						if(_alpm_key_import(handle, NULL, key) == -1) {
 							fail = 1;
 						}
 					}
diff --git a/lib/libalpm/signing.c b/lib/libalpm/signing.c
index 92598b0e..9dcd7cb8 100644
--- a/lib/libalpm/signing.c
+++ b/lib/libalpm/signing.c
@@ -259,14 +259,13 @@  error:

 /**
  * Search for a GPG key in a remote location.
- * This requires GPGME to call the gpg binary and have a keyserver previously
- * defined in a gpg.conf configuration file.
+ * This requires GPGME to call the gpg binary.
  * @param handle the context handle
  * @param fpr the fingerprint key ID to look up
  * @param pgpkey storage location for the given key if found
  * @return 1 on success, 0 on key not found, -1 on error
  */
-static int key_search(alpm_handle_t *handle, const char *fpr,
+static int key_search(alpm_handle_t *handle, const char *email, const char *fpr,
 		alpm_pgpkey_t *pgpkey)
 {
 	gpgme_error_t gpg_err;
@@ -276,6 +275,9 @@  static int key_search(alpm_handle_t *handle, const char *fpr,
 	int ret = -1;
 	size_t fpr_len;
 	char *full_fpr;
+	const char *tmpdir;
+	char *tmp_gnupgdir;
+	size_t tmp_gnupgdir_len;

 	/* gpg2 goes full retard here. For key searches ONLY, we need to prefix the
 	 * key fingerprint with 0x, or the lookup will fail. */
@@ -287,36 +289,92 @@  static int key_search(alpm_handle_t *handle, const char *fpr,
 	gpg_err = gpgme_new(&ctx);
 	CHECK_ERR();

-	mode = gpgme_get_keylist_mode(ctx);
-	/* using LOCAL and EXTERN together doesn't work for GPG 1.X. Ugh. */
-	mode &= ~GPGME_KEYLIST_MODE_LOCAL;
-	mode |= GPGME_KEYLIST_MODE_EXTERN;
-	gpg_err = gpgme_set_keylist_mode(ctx, mode);
-	CHECK_ERR();
-
-	_alpm_log(handle, ALPM_LOG_DEBUG, "looking up key %s remotely\n", fpr);
-
-	gpg_err = gpgme_get_key(ctx, full_fpr, &key, 0);
-	if(gpg_err_code(gpg_err) == GPG_ERR_EOF) {
-		_alpm_log(handle, ALPM_LOG_DEBUG, "key lookup failed, unknown key\n");
-		/* Try an alternate lookup using the 8 character fingerprint value, since
-		 * busted-ass keyservers can't support lookups using subkeys with the full
-		 * value as of now. This is why 2012 is not the year of PGP encryption. */
-		if(fpr_len > 8) {
-			const char *short_fpr = memcpy(&full_fpr[fpr_len - 8], "0x", 2);
-			_alpm_log(handle, ALPM_LOG_DEBUG,
-					"looking up key %s remotely\n", short_fpr);
-			gpg_err = gpgme_get_key(ctx, short_fpr, &key, 0);
-			if(gpg_err_code(gpg_err) == GPG_ERR_EOF) {
-				_alpm_log(handle, ALPM_LOG_DEBUG, "key lookup failed, unknown key\n");
-				ret = 0;
+	/* Using GPGME_KEYLIST_MODE_LOCATE to lookup a key using WKD directly
+	 * imports it into the keyring, so we use a temporary GnuPG home directory
+	 * to retrieve the key. */
+	tmpdir = getenv("TMPDIR");
+	if(!tmpdir) {
+		tmpdir = "/tmp";
+	}
+	tmp_gnupgdir_len = strlen(tmpdir) + strlen("/alpm_gnupg_XXXXXX") + 1;
+	MALLOC(tmp_gnupgdir, tmp_gnupgdir_len, RET_ERR(handle, ALPM_ERR_MEMORY, -1));
+	snprintf(tmp_gnupgdir, tmp_gnupgdir_len, "%s/alpm_gnupg_XXXXXX", tmpdir);
+	if(!mkdtemp(tmp_gnupgdir)) {
+		_alpm_log(handle, ALPM_LOG_DEBUG, "could not create temp directory\n");
+	} else {
+		gpg_err = gpgme_ctx_set_engine_info(ctx, GPGME_PROTOCOL_OpenPGP, NULL, tmp_gnupgdir);
+		CHECK_ERR();
+
+		mode = gpgme_get_keylist_mode(ctx);
+		mode |= GPGME_KEYLIST_MODE_LOCATE;
+		gpg_err = gpgme_set_keylist_mode(ctx, mode);
+		CHECK_ERR();
+		_alpm_log(handle, ALPM_LOG_DEBUG, "looking up key for %s using WKD\n", email);
+		gpg_err = gpgme_get_key(ctx, email, &key, 0);
+
+		if(gpg_err_code(gpg_err) == GPG_ERR_NO_ERROR) {
+			gpgme_subkey_t subkey = key->subkeys;
+			ret = 0;
+			while(subkey) {
+				if(strcmp(subkey->keyid, fpr) == 0) {
+					ret = 1;
+					pgpkey->location = 'w';
+					break;
+				}
+				subkey = subkey->next;
+			}
+			if(ret != 1) {
+				_alpm_log(handle, ALPM_LOG_DEBUG, "key ID mismatch on WKD\n");
 			}
 		} else {
+			_alpm_log(handle, ALPM_LOG_DEBUG, "key lookup failed, unknown key\n");
 			ret = 0;
 		}
+		rmrf(tmp_gnupgdir);
+		free(tmp_gnupgdir);
 	}

-	CHECK_ERR();
+	if(ret != 1) {
+		/* For keyserver lookup, we want to use the options specified in pacman's
+		 * gpg.conf, so we destroy the context with the temporary home directory
+		 * and recreate one with the default one set in init_gpgme. */
+		gpgme_release(ctx);
+		memset(&ctx, 0, sizeof(ctx));
+		gpg_err = gpgme_new(&ctx);
+		CHECK_ERR();
+
+		mode = gpgme_get_keylist_mode(ctx);
+		/* using LOCAL and EXTERN together doesn't work for GPG 1.X. Ugh. */
+		mode &= ~GPGME_KEYLIST_MODE_LOCAL;
+		mode |= GPGME_KEYLIST_MODE_EXTERN;
+		gpg_err = gpgme_set_keylist_mode(ctx, mode);
+		CHECK_ERR();
+
+		_alpm_log(handle, ALPM_LOG_DEBUG, "looking up key %s on keyserver\n", fpr);
+
+		gpg_err = gpgme_get_key(ctx, full_fpr, &key, 0);
+		if(gpg_err_code(gpg_err) == GPG_ERR_EOF) {
+			_alpm_log(handle, ALPM_LOG_DEBUG, "key lookup failed, unknown key\n");
+			/* Try an alternate lookup using the 8 character fingerprint value, since
+			 * busted-ass keyservers can't support lookups using subkeys with the full
+			 * value as of now. This is why 2012 is not the year of PGP encryption. */
+			if(fpr_len > 8) {
+				const char *short_fpr = memcpy(&full_fpr[fpr_len - 8], "0x", 2);
+				_alpm_log(handle, ALPM_LOG_DEBUG,
+						"looking up key %s on keyserver\n", short_fpr);
+				gpg_err = gpgme_get_key(ctx, short_fpr, &key, 0);
+				if(gpg_err_code(gpg_err) == GPG_ERR_EOF) {
+					_alpm_log(handle, ALPM_LOG_DEBUG, "key lookup failed, unknown key\n");
+					ret = 0;
+				}
+			} else {
+				ret = 0;
+			}
+		}
+
+		CHECK_ERR();
+		pgpkey->location = 'k';
+	}

 	/* should only get here if key actually exists */
 	pgpkey->data = key;
@@ -327,7 +385,15 @@  static int key_search(alpm_handle_t *handle, const char *fpr,
 	}
 	pgpkey->uid = key->uids->uid;
 	pgpkey->name = key->uids->name;
-	pgpkey->email = key->uids->email;
+	if(pgpkey->location == 'w') {
+		/* We have found the key in a WKD using the provided email address. To
+		 * make sure we can import it into the keyring later, record the email
+		 * address in the key. Otherwise the email address of a different,
+		 * non-WKD-enabled user ID might be used, leading to an import failure. */
+		pgpkey->email = strdup(email);
+	} else {
+		pgpkey->email = key->uids->email;
+	}
 	pgpkey->created = key->subkeys->timestamp;
 	pgpkey->expires = key->subkeys->expires;
 	pgpkey->length = key->subkeys->length;
@@ -396,6 +462,8 @@  static int key_import(alpm_handle_t *handle, alpm_pgpkey_t *key)
 	gpgme_key_t keys[2];
 	gpgme_import_result_t result;
 	int ret = -1;
+	gpgme_keylist_mode_t mode;
+	gpgme_key_t UNUSED gpgme_key;

 	if(_alpm_access(handle, handle->gpgdir, "pubring.gpg", W_OK)) {
 		/* no chance of import succeeding if pubring isn't writable */
@@ -409,20 +477,37 @@  static int key_import(alpm_handle_t *handle, alpm_pgpkey_t *key)

 	_alpm_log(handle, ALPM_LOG_DEBUG, "importing key\n");

-	keys[0] = key->data;
-	keys[1] = NULL;
-	gpg_err = gpgme_op_import_keys(ctx, keys);
-	CHECK_ERR();
-	result = gpgme_op_import_result(ctx);
-	/* we know we tried to import exactly one key, so check for this */
-	if(result->considered != 1 || !result->imports) {
-		_alpm_log(handle, ALPM_LOG_DEBUG, "could not import key, 0 results\n");
-		ret = -1;
-	} else if(result->imports->result != GPG_ERR_NO_ERROR) {
-		_alpm_log(handle, ALPM_LOG_DEBUG, "gpg error: %s\n", gpgme_strerror(gpg_err));
-		ret = -1;
-	} else {
-		ret = 0;
+	switch(key->location) {
+		case 'w':
+			mode = gpgme_get_keylist_mode(ctx);
+			mode |= GPGME_KEYLIST_MODE_LOCATE;
+			gpg_err = gpgme_set_keylist_mode(ctx, mode);
+			CHECK_ERR();
+			gpg_err = gpgme_get_key(ctx, key->email, &gpgme_key, 0);
+			free(gpgme_key);
+			CHECK_ERR();
+			ret = 0;
+			break;
+		case 'k':
+			keys[0] = key->data;
+			keys[1] = NULL;
+			gpg_err = gpgme_op_import_keys(ctx, keys);
+			CHECK_ERR();
+			result = gpgme_op_import_result(ctx);
+			/* we know we tried to import exactly one key, so check for this */
+			if(result->considered != 1 || !result->imports) {
+				_alpm_log(handle, ALPM_LOG_DEBUG, "could not import key, 0 results\n");
+				ret = -1;
+			} else if(result->imports->result != GPG_ERR_NO_ERROR) {
+				_alpm_log(handle, ALPM_LOG_DEBUG, "gpg error: %s\n", gpgme_strerror(gpg_err));
+				ret = -1;
+			} else {
+				ret = 0;
+			}
+			break;
+		default:
+			_alpm_log(handle, ALPM_LOG_DEBUG, "unknown key location '%c'\n", key->location);
+			ret = -1;
 	}

 gpg_error:
@@ -436,15 +521,15 @@  gpg_error:
  * @param fpr the fingerprint key ID to import
  * @return 0 on success, -1 on error
  */
-int _alpm_key_import(alpm_handle_t *handle, const char *fpr)
+int _alpm_key_import(alpm_handle_t *handle, const char *email, const char *fpr)
 {
 	int ret = -1;
 	alpm_pgpkey_t fetch_key;
 	memset(&fetch_key, 0, sizeof(fetch_key));

-	if(key_search(handle, fpr, &fetch_key) == 1) {
+	if(key_search(handle, email, fpr, &fetch_key) == 1) {
 		_alpm_log(handle, ALPM_LOG_DEBUG,
-				"unknown key, found %s on keyserver\n", fetch_key.uid);
+				"unknown key, found %s remotely\n", fetch_key.uid);
 		if(!_alpm_access(handle, handle->gpgdir, "pubring.gpg", W_OK)) {
 			alpm_question_import_key_t question = {
 				.type = ALPM_QUESTION_IMPORT_KEY,
@@ -463,7 +548,7 @@  int _alpm_key_import(alpm_handle_t *handle, const char *fpr)
 		} else {
 			/* keyring directory was not writable, so we don't even try */
 			_alpm_log(handle, ALPM_LOG_WARNING,
-					_("key %s, \"%s\" found on keyserver, keyring is not writable\n"),
+					_("key %s, \"%s\" found remotely, keyring is not writable\n"),
 					fetch_key.fingerprint, fetch_key.uid);
 		}
 	} else {
@@ -714,7 +799,8 @@  int _alpm_key_in_keychain(alpm_handle_t UNUSED *handle, const char UNUSED *fpr)
 	return -1;
 }

-int _alpm_key_import(alpm_handle_t UNUSED *handle, const char UNUSED *fpr)
+int _alpm_key_import(alpm_handle_t UNUSED *handle, const char UNUSED *email,
+		const char UNUSED *fpr)
 {
 	return -1;
 }
@@ -900,7 +986,9 @@  int _alpm_process_siglist(alpm_handle_t *handle, const char *identifier,
 				_alpm_log(handle, ALPM_LOG_ERROR,
 						_("%s: key \"%s\" is unknown\n"), identifier, name);

-				if(_alpm_key_import(handle, result->key.fingerprint) == 0) {
+				/* No information on the User ID is available, so we cannot search by
+				 * email address. */
+				if(_alpm_key_import(handle, NULL, result->key.fingerprint) == 0) {
 					retry = 1;
 				}

diff --git a/lib/libalpm/signing.h b/lib/libalpm/signing.h
index f8b84b94..d67709da 100644
--- a/lib/libalpm/signing.h
+++ b/lib/libalpm/signing.h
@@ -32,6 +32,6 @@  int _alpm_process_siglist(alpm_handle_t *handle, const char *identifier,
 		alpm_siglist_t *siglist, int optional, int marginal, int unknown);

 int _alpm_key_in_keychain(alpm_handle_t *handle, const char *fpr);
-int _alpm_key_import(alpm_handle_t *handle, const char *fpr);
+int _alpm_key_import(alpm_handle_t *handle, const char *email, const char *fpr);

 #endif /* ALPM_SIGNING_H */
diff --git a/lib/libalpm/sync.c b/lib/libalpm/sync.c
index cbd072e6..efad77ba 100644
--- a/lib/libalpm/sync.c
+++ b/lib/libalpm/sync.c
@@ -927,7 +927,7 @@  static int check_keyring(alpm_handle_t *handle)
 		alpm_list_t *k;
 		for(k = errors; k; k = k->next) {
 			char *key = k->data;
-			if(_alpm_key_import(handle, key) == -1) {
+			if(_alpm_key_import(handle, NULL, key) == -1) {
 				fail = 1;
 			}
 		}