[pacman-dev] [PATCH] Add per-repo PinnedPubKey option

Travis Burtrum travis.archlinux at burtrum.org
Mon Oct 31 20:36:23 UTC 2016


>From abb057844eec0e5707c31b643d0f2187b4cf0eb6 Mon Sep 17 00:00:00 2001
From: Travis Burtrum <travis.archlinux at burtrum.org>
Date: Mon, 31 Oct 2016 02:12:31 -0400
Subject: [PATCH] Add per-repo PinnedPubKey option

This sets curl's CURLOPT_PINNEDPUBLICKEY option in the built-in
downloader, or replaces %p in XferCommand.  This pins public
keys to ensure your TLS connection is not man-in-the-middled
without relying on CAs etc.  Probably most useful currently
for very small groups or single servers.

It would obviously be best as a per-mirror option, but such
a thing currently does not exist.

Signed-off-by: Travis Burtrum <travis.archlinux at burtrum.org>
---
 doc/pacman.conf.5.txt | 12 +++++++++++-
 lib/libalpm/alpm.h    | 10 +++++++++-
 lib/libalpm/be_sync.c |  4 ++++
 lib/libalpm/db.c      | 12 ++++++++++++
 lib/libalpm/db.h      |  1 +
 lib/libalpm/dload.c   |  8 +++++++-
 lib/libalpm/dload.h   |  1 +
 lib/libalpm/sync.c    |  7 ++++---
 src/pacman/conf.c     | 26 ++++++++++++++++++++++++--
 src/pacman/conf.h     |  1 +
 10 files changed, 74 insertions(+), 8 deletions(-)

diff --git a/doc/pacman.conf.5.txt b/doc/pacman.conf.5.txt
index c665870..b1859c7 100644
--- a/doc/pacman.conf.5.txt
+++ b/doc/pacman.conf.5.txt
@@ -126,7 +126,8 @@ Options
 	All instances of `%u` will be replaced with the download URL. If present,
 	instances of `%o` will be replaced with the local filename, plus a
 	``.part'' extension, which allows programs like wget to do file resumes
-	properly.
+	properly. If present, instances of `%p` will be replaced with the value of
+	the repo specific PinnedPubKey directive, or `` if that is not set.
 	+
 	This option is useful for users who experience problems with built-in
 	HTTP/FTP support, or need the more advanced proxy support that comes with
@@ -276,6 +277,15 @@ even be used for different architectures.
 Note that an enabled repository can be operated on explicitly, regardless of the Usage
 level set.
 
+*PinnedPubKey =* pinnedpubkey::
+	The string can be the file name of your pinned public key. The file format expected
+	is "PEM" or "DER".  The string can also be any number of base64 encoded sha256 
+	hashes preceded by "sha256//" and separated by ";"
++
+When negotiating a TLS or SSL connection, the server sends a certificate indicating
+its identity. A public key is extracted from this certificate and if it does not
+exactly match the public key provided to this option, pacman will abort the
+connection before sending or receiving any data.
 
 Package and Database Signature Checking[[SC]]
 ---------------------------------------------
diff --git a/lib/libalpm/alpm.h b/lib/libalpm/alpm.h
index 2d2491d..e942559 100644
--- a/lib/libalpm/alpm.h
+++ b/lib/libalpm/alpm.h
@@ -750,11 +750,12 @@ typedef void (*alpm_cb_totaldl)(off_t total);
  * @param url the URL of the file to be downloaded
  * @param localpath the directory to which the file should be downloaded
  * @param force whether to force an update, even if the file is the same
+ * @param pinnedpubkey a pinned public key string
  * @return 0 on success, 1 if the file exists and is identical, -1 on
  * error.
  */
 typedef int (*alpm_cb_fetch)(const char *url, const char *localpath,
-		int force);
+		int force, const char *pinnedpubkey);
 
 /** Fetch a remote pkg.
  * @param handle the context handle
@@ -1037,6 +1038,13 @@ alpm_list_t *alpm_db_get_groupcache(alpm_db_t *db);
  */
 alpm_list_t *alpm_db_search(alpm_db_t *db, const alpm_list_t *needles);
 
+/** Sets the pinned public key of a database.
+ * @param db pointer to the package database to set the status for
+ * @param pinnedpubkey a pinned public key string
+ * @return 0 on success, or -1 on error
+ */
+int alpm_db_set_pinnedpubkey(alpm_db_t *db, char *pinnedpubkey);
+
 typedef enum _alpm_db_usage_ {
 	ALPM_DB_USAGE_SYNC = 1,
 	ALPM_DB_USAGE_SEARCH = (1 << 1),
diff --git a/lib/libalpm/be_sync.c b/lib/libalpm/be_sync.c
index 7774975..be2cd64 100644
--- a/lib/libalpm/be_sync.c
+++ b/lib/libalpm/be_sync.c
@@ -242,6 +242,8 @@ int SYMEXPORT alpm_db_update(int force, alpm_db_t *db)
 		payload.handle = handle;
 		payload.force = force;
 		payload.unlink_on_fail = 1;
+		
+		payload.pinnedpubkey = db->pinnedpubkey;
 
 		ret = _alpm_download(&payload, syncpath, NULL, &final_db_url);
 		_alpm_dload_payload_reset(&payload);
@@ -297,6 +299,8 @@ int SYMEXPORT alpm_db_update(int force, alpm_db_t *db)
 			/* set hard upper limit of 16KiB */
 			payload.max_size = 16 * 1024;
 
+			payload.pinnedpubkey = db->pinnedpubkey;
+
 			sig_ret = _alpm_download(&payload, syncpath, NULL, NULL);
 			/* errors_ok suppresses error messages, but not the return code */
 			sig_ret = payload.errors_ok ? 0 : sig_ret;
diff --git a/lib/libalpm/db.c b/lib/libalpm/db.c
index 5d78ee5..0dab1e7 100644
--- a/lib/libalpm/db.c
+++ b/lib/libalpm/db.c
@@ -306,6 +306,15 @@ alpm_list_t SYMEXPORT *alpm_db_search(alpm_db_t *db, const alpm_list_t *needles)
 	return _alpm_db_search(db, needles);
 }
 
+/** Sets the pinned public key for a repo */
+int SYMEXPORT alpm_db_set_pinnedpubkey(alpm_db_t *db, char *pinnedpubkey)
+{
+	ASSERT(db != NULL, return -1);
+	ASSERT(pinnedpubkey != NULL, return 0);
+	db->pinnedpubkey = strdup(pinnedpubkey);
+	return 0;
+}
+
 /** Sets the usage bitmask for a repo */
 int SYMEXPORT alpm_db_set_usage(alpm_db_t *db, int usage)
 {
@@ -351,6 +360,9 @@ void _alpm_db_free(alpm_db_t *db)
 	FREELIST(db->servers);
 	FREE(db->_path);
 	FREE(db->treename);
+	if(db->pinnedpubkey != NULL) {
+		FREE(db->pinnedpubkey);
+	}
 	FREE(db);
 
 	return;
diff --git a/lib/libalpm/db.h b/lib/libalpm/db.h
index 8e50196..4ce98c8 100644
--- a/lib/libalpm/db.h
+++ b/lib/libalpm/db.h
@@ -67,6 +67,7 @@ struct __alpm_db_t {
 	char *treename;
 	/* do not access directly, use _alpm_db_path(db) for lazy access */
 	char *_path;
+	char *pinnedpubkey;
 	alpm_pkghash_t *pkgcache;
 	alpm_list_t *grpcache;
 	alpm_list_t *servers;
diff --git a/lib/libalpm/dload.c b/lib/libalpm/dload.c
index 9d80358..6d5b371 100644
--- a/lib/libalpm/dload.c
+++ b/lib/libalpm/dload.c
@@ -327,6 +327,12 @@ static void curl_set_handle_opts(struct dload_payload *payload,
 		curl_easy_setopt(curl, CURLOPT_USERAGENT, useragent);
 	}
 
+	if(payload->pinnedpubkey != NULL) {
+		_alpm_log(handle, ALPM_LOG_DEBUG,
+				"using curl pinnedpubkey: %s\n", payload->pinnedpubkey);
+		curl_easy_setopt(curl, CURLOPT_PINNEDPUBLICKEY, payload->pinnedpubkey);
+	}
+
 	if(!payload->allow_resume && !payload->force && payload->destfile_name &&
 			stat(payload->destfile_name, &st) == 0) {
 		/* start from scratch, but only download if our local is out of date. */
@@ -646,7 +652,7 @@ int _alpm_download(struct dload_payload *payload, const char *localpath,
 		RET_ERR(handle, ALPM_ERR_EXTERNAL_DOWNLOAD, -1);
 #endif
 	} else {
-		int ret = handle->fetchcb(payload->fileurl, localpath, payload->force);
+		int ret = handle->fetchcb(payload->fileurl, localpath, payload->force, payload->pinnedpubkey);
 		if(ret == -1 && !payload->errors_ok) {
 			RET_ERR(handle, ALPM_ERR_EXTERNAL_DOWNLOAD, -1);
 		}
diff --git a/lib/libalpm/dload.h b/lib/libalpm/dload.h
index 427c486..b938fdc 100644
--- a/lib/libalpm/dload.h
+++ b/lib/libalpm/dload.h
@@ -41,6 +41,7 @@ struct dload_payload {
 	int errors_ok;
 	int unlink_on_fail;
 	int trust_remote_name;
+	char *pinnedpubkey;
 #ifdef HAVE_LIBCURL
 	CURLcode curlerr;       /* last error produced by curl */
 #endif
diff --git a/lib/libalpm/sync.c b/lib/libalpm/sync.c
index 837639d..05e1ffe 100644
--- a/lib/libalpm/sync.c
+++ b/lib/libalpm/sync.c
@@ -862,7 +862,7 @@ static int validate_deltas(alpm_handle_t *handle, alpm_list_t *deltas)
 }
 
 static struct dload_payload *build_payload(alpm_handle_t *handle,
-		const char *filename, size_t size, alpm_list_t *servers)
+		const char *filename, size_t size, alpm_list_t *servers, char *pinnedpubkey)
 {
 		struct dload_payload *payload;
 
@@ -870,6 +870,7 @@ static struct dload_payload *build_payload(alpm_handle_t *handle,
 		STRDUP(payload->remote_name, filename, FREE(payload); RET_ERR(handle, ALPM_ERR_MEMORY, NULL));
 		payload->max_size = size;
 		payload->servers = servers;
+		payload->pinnedpubkey = pinnedpubkey;
 		return payload;
 }
 
@@ -898,7 +899,7 @@ static int find_dl_candidates(alpm_db_t *repo, alpm_list_t **files, alpm_list_t
 					alpm_delta_t *delta = dlts->data;
 					if(delta->download_size != 0) {
 						struct dload_payload *payload = build_payload(
-								handle, delta->delta, delta->delta_size, repo->servers);
+								handle, delta->delta, delta->delta_size, repo->servers, repo->pinnedpubkey);
 						ASSERT(payload, return -1);
 						*files = alpm_list_add(*files, payload);
 					}
@@ -909,7 +910,7 @@ static int find_dl_candidates(alpm_db_t *repo, alpm_list_t **files, alpm_list_t
 			} else if(spkg->download_size != 0) {
 				struct dload_payload *payload;
 				ASSERT(spkg->filename != NULL, RET_ERR(handle, ALPM_ERR_PKG_INVALID_NAME, -1));
-				payload = build_payload(handle, spkg->filename, spkg->size, repo->servers);
+				payload = build_payload(handle, spkg->filename, spkg->size, repo->servers, repo->pinnedpubkey);
 				ASSERT(payload, return -1);
 				*files = alpm_list_add(*files, payload);
 			}
diff --git a/src/pacman/conf.c b/src/pacman/conf.c
index d8d64fb..8dd23e5 100644
--- a/src/pacman/conf.c
+++ b/src/pacman/conf.c
@@ -163,6 +163,9 @@ void config_repo_free(config_repo_t *repo)
 	if(repo == NULL) {
 		return;
 	}
+	if(repo->pinnedpubkey != NULL) {
+		free(repo->pinnedpubkey);
+	}
 	free(repo->name);
 	FREELIST(repo->servers);
 	free(repo);
@@ -204,7 +207,7 @@ static char *get_tempfile(const char *path, const char *filename)
 
 /** External fetch callback */
 static int download_with_xfercommand(const char *url, const char *localpath,
-		int force)
+		int force, const char *pinnedpubkey)
 {
 	int ret = 0, retval;
 	int usepart = 0;
@@ -232,6 +235,16 @@ static int download_with_xfercommand(const char *url, const char *localpath,
 	}
 
 	tempcmd = strdup(config->xfercommand);
+	/* replace all occurrences of %p with pinnedpubkey */
+	if(strstr(tempcmd, "%p")) {
+		if(pinnedpubkey == NULL) {
+			parsedcmd = strreplace(tempcmd, "%p", "");
+		} else {
+			parsedcmd = strreplace(tempcmd, "%p", pinnedpubkey);
+		}
+		free(tempcmd);
+		tempcmd = parsedcmd;
+	}
 	/* replace all occurrences of %o with fn.part */
 	if(strstr(tempcmd, "%o")) {
 		usepart = 1;
@@ -668,6 +681,8 @@ static int register_repo(config_repo_t *repo)
 			repo->name);
 	alpm_db_set_usage(db, repo->usage == 0 ? ALPM_DB_USAGE_ALL : repo->usage);
 
+	alpm_db_set_pinnedpubkey(db, repo->pinnedpubkey);
+
 	for(i = repo->servers; i; i = alpm_list_next(i)) {
 		char *value = i->data;
 		if(_add_mirror(db, value) != 0) {
@@ -915,12 +930,19 @@ static int _parse_repo(const char *key, char *value, const char *file,
 			}
 			FREELIST(values);
 		}
+	} else if(strcmp(key, "PinnedPubKey") == 0) {
+		if(!value) {
+			pm_printf(ALPM_LOG_ERROR, _("config file %s, line %d: directive '%s' needs a value\n"),
+					file, line, key);
+		} else {
+			repo->pinnedpubkey = strdup(value);
+			pm_printf(ALPM_LOG_DEBUG, "repo config: pinnedpubkey: %s\n", value);
+		}
 	} else {
 		pm_printf(ALPM_LOG_WARNING,
 				_("config file %s, line %d: directive '%s' in section '%s' not recognized.\n"),
 				file, line, key, repo->name);
 	}
-
 	return ret;
 }
 
diff --git a/src/pacman/conf.h b/src/pacman/conf.h
index 945de7c..7c021bf 100644
--- a/src/pacman/conf.h
+++ b/src/pacman/conf.h
@@ -40,6 +40,7 @@ typedef struct __config_repo_t {
 	int usage;
 	int siglevel;
 	int siglevel_mask;
+	char *pinnedpubkey;
 } config_repo_t;
 
 typedef struct __config_t {
-- 
2.10.2


More information about the pacman-dev mailing list