"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Josh Rickmar <openbsd+lists@zettaport.com>
Subject:
Tag signing with SSH signatures
To:
gameoftrees@openbsd.org
Date:
Thu, 30 Jun 2022 07:37:22 -0400

Download raw body.

Thread
Here's a first try at creating and verifying tags signed by SSH
signatures.  I've yet to write the documentation or regress tests but
eager others can begin to look over the implementation.  Feedback
welcome :)

A new -s flag is added to got tag when creating signatures which
specifies which key to use to sign with.  For example:

$ got tag -s /home/jrick/.ssh/id_ed25519 tag_name

The SSH signature appears at the end of the tag message.  This is what
git also does when signing with SSH keys.  Right now we are not
clipping this signature from any displayed tag messages, but that can
be done.

Verification is done with a new -V flag introduced to got tag.  It
also requires creating an allowed signers file (see ssh-keygen(1)) and
providing the path in your got.conf(5) with a new allowed_signers
directive.

$ cat .got/got.conf        
allowed_signers "/home/jrick/allowed_signers"
$ cat /home/jrick/allowed_signers  
jrick@zettaport.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMfooXuB1k6zimkfBzbMTTuRupF3u+56Pun4pJmEQuTI
$ got tag -V sig_test
-----------------------------------------------
tag sig_test d2de9fffa937cbff70ffe59494acdecda86224b6
from: Josh Rickmar <jrick@zettaport.com>
date: Thu Jun 30 11:20:00 2022 UTC
object: commit 9b058f456d15d60a89334ce3e7f0a7c22e182c55
signature: Good "git" signature for jrick@zettaport.com with ED25519 key SHA256:LCWFweH6pA7DYiP/WMDYthnbM3HiOJBxG0/29179Fkg
 
 hi
 -----BEGIN SSH SIGNATURE-----
 U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgx+ihe4HWTrOKaR8HNsxNO5G6kX
 e77no+6fikmYRC5MgAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
 AAAAQOkShDme1WiGDAz+DAIpcjEZSIblFFeOJsTpoJPYo409kJGmTuxh9vfth0MrrtyZ0k
 eEY9I0BIfQ+czIriAnaAs=
 -----END SSH SIGNATURE-----

diff refs/heads/main refs/heads/ssh_sigs
commit - 9b058f456d15d60a89334ce3e7f0a7c22e182c55
commit + cebbdb6cf3dcd1c91cd6551c00afa271ea77c2e2
blob - 8dfd844f3da472b6ed040a62acaf85403cbc07ea
blob + 21f8d657a812ed856524bf6db776b0534b6939b4
--- got/Makefile
+++ got/Makefile
@@ -13,7 +13,7 @@ SRCS=		got.c blame.c commit_graph.c delta.c diff.c \
 		diff_myers.c diff_output.c diff_output_plain.c \
 		diff_output_unidiff.c diff_output_edscript.c \
 		diff_patience.c send.c deltify.c pack_create.c dial.c \
-		bloom.c murmurhash2.c ratelimit.c patch.c
+		bloom.c murmurhash2.c ratelimit.c patch.c tag.c date.c
 
 MAN =		${PROG}.1 got-worktree.5 git-repository.5 got.conf.5
 
blob - 2dceeed3a7a54bee61a491f92527df8295059f95
blob + 422dbc8d83e81efe3f1f4faef1c59cc7f9ac72db
--- got/got.c
+++ got/got.c
@@ -58,6 +58,8 @@
 #include "got_gotconfig.h"
 #include "got_dial.h"
 #include "got_patch.h"
+#include "got_tag.h"
+#include "got_date.h"
 
 #ifndef nitems
 #define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
@@ -687,6 +689,76 @@ get_author(char **author, struct got_repository *repo,
 }
 
 static const struct got_error *
+get_allowed_signers(char **allowed_signers, struct got_repository *repo,
+    struct got_worktree *worktree)
+{
+	const char *got_allowed_signers = NULL;
+	const struct got_gotconfig *worktree_conf = NULL, *repo_conf = NULL;
+
+	*allowed_signers = NULL;
+
+	if (worktree)
+		worktree_conf = got_worktree_get_gotconfig(worktree);
+	repo_conf = got_repo_get_gotconfig(repo);
+
+	/*
+	 * Priority of potential author information sources, from most
+	 * significant to least significant:
+	 * 1) work tree's .got/got.conf file
+	 * 2) repository's got.conf file
+	 */
+
+	if (worktree_conf)
+		got_allowed_signers = got_gotconfig_get_allowed_signers_file(
+		    worktree_conf);
+	if (got_allowed_signers == NULL)
+		got_allowed_signers = got_gotconfig_get_allowed_signers_file(
+		    repo_conf);
+
+	if (got_allowed_signers) {
+		*allowed_signers = strdup(got_allowed_signers);
+		if (*allowed_signers == NULL)
+			return got_error_from_errno("strdup");
+	}
+	return NULL;
+}
+
+static const struct got_error *
+get_revoked_signers(char **revoked_signers, struct got_repository *repo,
+    struct got_worktree *worktree)
+{
+	const char *got_revoked_signers = NULL;
+	const struct got_gotconfig *worktree_conf = NULL, *repo_conf = NULL;
+
+	*revoked_signers = NULL;
+
+	if (worktree)
+		worktree_conf = got_worktree_get_gotconfig(worktree);
+	repo_conf = got_repo_get_gotconfig(repo);
+
+	/*
+	 * Priority of potential author information sources, from most
+	 * significant to least significant:
+	 * 1) work tree's .got/got.conf file
+	 * 2) repository's got.conf file
+	 */
+
+	if (worktree_conf)
+		got_revoked_signers = got_gotconfig_get_revoked_signers_file(
+		    worktree_conf);
+	if (got_revoked_signers == NULL)
+		got_revoked_signers = got_gotconfig_get_revoked_signers_file(
+		    repo_conf);
+
+	if (got_revoked_signers) {
+		*revoked_signers = strdup(got_revoked_signers);
+		if (*revoked_signers == NULL)
+			return got_error_from_errno("strdup");
+	}
+	return NULL;
+}
+
+static const struct got_error *
 get_gitconfig_path(char **gitconfig_path)
 {
 	const char *homedir = getenv("HOME");
@@ -6807,7 +6879,8 @@ usage_tag(void)
 {
 	fprintf(stderr,
 	    "usage: %s tag [-c commit] [-r repository] [-l] "
-	        "[-m message] name\n", getprogname());
+	        "[-m message] [-s signer_id] name\n",
+	        getprogname());
 	exit(1);
 }
 
@@ -6887,7 +6960,8 @@ get_tag_refname(char **refname, const char *tag_name)
 }
 
 static const struct got_error *
-list_tags(struct got_repository *repo, const char *tag_name)
+list_tags(struct got_repository *repo, const char *tag_name, int verify_tags,
+    const char *allowed_signers, const char *revoked_signers, int verbosity)
 {
 	static const struct got_error *err = NULL;
 	struct got_reflist_head refs;
@@ -6916,7 +6990,8 @@ list_tags(struct got_repository *repo, const char *tag
 		const char *refname;
 		char *refstr, *tagmsg0, *tagmsg, *line, *id_str, *datestr;
 		char datebuf[26];
-		const char *tagger;
+		const char *tagger, *ssh_sig = NULL;
+		char *sig_msg = NULL;
 		time_t tagger_time;
 		struct got_object_id *id;
 		struct got_tag_object *tag;
@@ -6932,8 +7007,6 @@ list_tags(struct got_repository *repo, const char *tag
 			err = got_error_from_errno("got_ref_to_str");
 			break;
 		}
-		printf("%stag %s %s\n", GOT_COMMIT_SEP_STR, refname, refstr);
-		free(refstr);
 
 		err = got_ref_resolve(&id, repo, re->ref);
 		if (err)
@@ -6966,6 +7039,22 @@ list_tags(struct got_repository *repo, const char *tag
 			if (err)
 				break;
 		}
+
+		if (verify_tags) {
+			ssh_sig = got_tag_message_get_ssh_signature(
+			    got_object_tag_get_message(tag));
+			if (ssh_sig && allowed_signers == NULL) {
+				err = got_error_msg(
+				    GOT_ERR_CANTVERIFY_TAG_SIGNATURE,
+				    "SSH signature verification requires "
+				        "setting allowed_signers in "
+				        "got.conf(5)");
+				break;
+			}
+		}
+
+		printf("%stag %s %s\n", GOT_COMMIT_SEP_STR, refname, refstr);
+		free(refstr);
 		printf("from: %s\n", tagger);
 		datestr = get_datestr(&tagger_time, datebuf);
 		if (datestr)
@@ -6995,6 +7084,17 @@ list_tags(struct got_repository *repo, const char *tag
 			}
 		}
 		free(id_str);
+
+		if (ssh_sig) {
+			err = got_tag_verify_ssh(&sig_msg, tag, ssh_sig,
+				allowed_signers, revoked_signers, 0);
+			if (err)
+				break;
+			printf("signature: %s", sig_msg);
+			free(sig_msg);
+			sig_msg = NULL;
+		}
+
 		if (commit) {
 			err = got_object_commit_get_logmsg(&tagmsg0, commit);
 			if (err)
@@ -7068,9 +7168,6 @@ done:
 	if (fd != -1 && close(fd) == -1 && err == NULL)
 		err = got_error_from_errno2("close", *tagmsg_path);
 
-	/* Editor is done; we can now apply unveil(2) */
-	if (err == NULL)
-		err = apply_unveil(repo_path, 0, NULL);
 	if (err) {
 		free(*tagmsg);
 		*tagmsg = NULL;
@@ -7080,7 +7177,8 @@ done:
 
 static const struct got_error *
 add_tag(struct got_repository *repo, const char *tagger,
-    const char *tag_name, const char *commit_arg, const char *tagmsg_arg)
+    const char *tag_name, const char *commit_arg, const char *tagmsg_arg,
+    const char *key_file, int verbosity)
 {
 	const struct got_error *err = NULL;
 	struct got_object_id *commit_id = NULL, *tag_id = NULL;
@@ -7136,10 +7234,18 @@ add_tag(struct got_repository *repo, const char *tagge
 				preserve_tagmsg = 1;
 			goto done;
 		}
+		/* Editor is done; we can now apply unveil(2) */
+		err = got_tag_apply_unveil();
+		if (err)
+			goto done;
+		err = apply_unveil(got_repo_get_path(repo), 0, NULL);
+		if (err)
+			goto done;
 	}
 
 	err = got_object_tag_create(&tag_id, tag_name, commit_id,
-	    tagger, time(NULL), tagmsg ? tagmsg : tagmsg_arg, repo);
+	    tagger, time(NULL), tagmsg ? tagmsg : tagmsg_arg, key_file, repo,
+	    verbosity);
 	if (err) {
 		if (tagmsg_path)
 			preserve_tagmsg = 1;
@@ -7193,11 +7299,13 @@ cmd_tag(int argc, char *argv[])
 	struct got_worktree *worktree = NULL;
 	char *cwd = NULL, *repo_path = NULL, *commit_id_str = NULL;
 	char *gitconfig_path = NULL, *tagger = NULL;
+	char *allowed_signers = NULL, *revoked_signers = NULL;
 	const char *tag_name = NULL, *commit_id_arg = NULL, *tagmsg = NULL;
-	int ch, do_list = 0;
+	int ch, do_list = 0, verify_tags = 0, verbosity = 0;
+	const char *signer_id = NULL;
 	int *pack_fds = NULL;
 
-	while ((ch = getopt(argc, argv, "c:m:r:l")) != -1) {
+	while ((ch = getopt(argc, argv, "c:m:r:ls:Vv")) != -1) {
 		switch (ch) {
 		case 'c':
 			commit_id_arg = optarg;
@@ -7215,6 +7323,18 @@ cmd_tag(int argc, char *argv[])
 		case 'l':
 			do_list = 1;
 			break;
+		case 's':
+			signer_id = optarg;
+			break;
+		case 'V':
+			verify_tags = 1;
+			break;
+		case 'v':
+			if (verbosity < 0)
+				verbosity = 0;
+			else if (verbosity < 3)
+				verbosity++;
+			break;
 		default:
 			usage_tag();
 			/* NOTREACHED */
@@ -7275,26 +7395,40 @@ cmd_tag(int argc, char *argv[])
 		}
 	}
 
-	if (do_list) {
+	if (do_list || verify_tags) {
+		error = got_repo_open(&repo, repo_path, NULL, pack_fds);
+		if (error != NULL)
+			goto done;
+		error = get_allowed_signers(&allowed_signers, repo, worktree);
+		if (error)
+			goto done;
+		error = get_revoked_signers(&revoked_signers, repo, worktree);
+		if (error)
+			goto done;
 		if (worktree) {
 			/* Release work tree lock. */
 			got_worktree_close(worktree);
 			worktree = NULL;
 		}
-		error = got_repo_open(&repo, repo_path, NULL, pack_fds);
-		if (error != NULL)
-			goto done;
 
+		/*
+		 * Remove "cpath" promise unless needed for signature tmpfile
+		 * creation.
+		 */
+		if (verify_tags)
+		 	got_tag_apply_unveil();
+		else {
 #ifndef PROFILE
-		/* Remove "cpath" promise. */
-		if (pledge("stdio rpath wpath flock proc exec sendfd unveil",
-		    NULL) == -1)
-			err(1, "pledge");
+			if (pledge("stdio rpath wpath flock proc exec sendfd "
+			    "unveil", NULL) == -1)
+				err(1, "pledge");
 #endif
+		}
 		error = apply_unveil(got_repo_get_path(repo), 1, NULL);
 		if (error)
 			goto done;
-		error = list_tags(repo, tag_name);
+		error = list_tags(repo, tag_name, verify_tags, allowed_signers,
+		    revoked_signers, verbosity);
 	} else {
 		error = get_gitconfig_path(&gitconfig_path);
 		if (error)
@@ -7314,6 +7448,11 @@ cmd_tag(int argc, char *argv[])
 		}
 
 		if (tagmsg) {
+			if (signer_id) {
+				error = got_tag_apply_unveil();
+				if (error)
+					goto done;
+			}
 			error = apply_unveil(got_repo_get_path(repo), 0, NULL);
 			if (error)
 				goto done;
@@ -7338,7 +7477,8 @@ cmd_tag(int argc, char *argv[])
 		}
 
 		error = add_tag(repo, tagger, tag_name,
-		    commit_id_str ? commit_id_str : commit_id_arg, tagmsg);
+		    commit_id_str ? commit_id_str : commit_id_arg, tagmsg,
+		    signer_id, verbosity);
 	}
 done:
 	if (repo) {
@@ -7359,6 +7499,8 @@ done:
 	free(gitconfig_path);
 	free(commit_id_str);
 	free(tagger);
+	free(allowed_signers);
+	free(revoked_signers);
 	return error;
 }
 
@@ -12388,22 +12530,6 @@ cat_tree(struct got_object_id *id, struct got_reposito
 	return err;
 }
 
-static void
-format_gmtoff(char *buf, size_t sz, time_t gmtoff)
-{
-	long long h, m;
-	char sign = '+';
-
-	if (gmtoff < 0) {
-		sign = '-';
-		gmtoff = -gmtoff;
-	}
-
-	h = (long long)gmtoff / 3600;
-	m = ((long long)gmtoff - h*3600) / 60;
-	snprintf(buf, sz, "%c%02lld%02lld", sign, h, m);
-}
-
 static const struct got_error *
 cat_commit(struct got_object_id *id, struct got_repository *repo, FILE *outfile)
 {
@@ -12435,14 +12561,14 @@ cat_commit(struct got_object_id *id, struct got_reposi
 		fprintf(outfile, "%s%s\n", GOT_COMMIT_LABEL_PARENT, pid_str);
 		free(pid_str);
 	}
-	format_gmtoff(gmtoff, sizeof(gmtoff),
+	got_format_gmtoff(gmtoff, sizeof(gmtoff),
 	    got_object_commit_get_author_gmtoff(commit));
 	fprintf(outfile, "%s%s %lld %s\n", GOT_COMMIT_LABEL_AUTHOR,
 	    got_object_commit_get_author(commit),
 	    (long long)got_object_commit_get_author_time(commit),
 	    gmtoff);
 
-	format_gmtoff(gmtoff, sizeof(gmtoff),
+	got_format_gmtoff(gmtoff, sizeof(gmtoff),
 	    got_object_commit_get_committer_gmtoff(commit));
 	fprintf(outfile, "%s%s %lld %s\n", GOT_COMMIT_LABEL_COMMITTER,
 	    got_object_commit_get_author(commit),
@@ -12501,7 +12627,7 @@ cat_tag(struct got_object_id *id, struct got_repositor
 	fprintf(outfile, "%s%s\n", GOT_TAG_LABEL_TAG,
 	    got_object_tag_get_name(tag));
 
-	format_gmtoff(gmtoff, sizeof(gmtoff),
+	got_format_gmtoff(gmtoff, sizeof(gmtoff),
 	    got_object_tag_get_tagger_gmtoff(tag));
 	fprintf(outfile, "%s%s %lld %s\n", GOT_TAG_LABEL_TAGGER,
 	    got_object_tag_get_tagger(tag),
blob - 781133bbc9837ad999231c521ae9da3239c0232b
blob + f9ea174585d6eddc3416290cb385a2b4c190ac41
--- gotadmin/Makefile
+++ gotadmin/Makefile
@@ -8,7 +8,8 @@ SRCS=		gotadmin.c \
 		inflate.c lockfile.c object.c object_cache.c object_create.c \
 		object_idset.c object_parse.c opentemp.c pack.c pack_create.c \
 		path.c privsep.c reference.c repository.c repository_admin.c \
-		worktree_open.c sha1.c bloom.c murmurhash2.c ratelimit.c
+		worktree_open.c sha1.c bloom.c murmurhash2.c ratelimit.c tag.c \
+		buf.c date.c
 MAN =		${PROG}.1
 
 CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib
blob - /dev/null
blob + b377deed4284643517139184442dbef62e88b453 (mode 644)
--- /dev/null
+++ include/got_date.h
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2022 Josh Rickmar <jrick@zettaport.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+void
+got_format_gmtoff(char *, size_t, time_t);
blob - 22a9264b9f8d0c0b20b48895dd8ea59708e61d48
blob + 818d096c51e0f1978febbe41ed56f812cc1f33e6
--- include/got_error.h
+++ include/got_error.h
@@ -169,6 +169,8 @@
 #define GOT_ERR_PATCH_FAILED	151
 #define GOT_ERR_FILEIDX_DUP_ENTRY 152
 #define GOT_ERR_PIN_PACK	153
+#define GOT_ERR_BAD_TAG_SIGNATURE 154
+#define GOT_ERR_CANTVERIFY_TAG_SIGNATURE 155
 
 struct got_error {
         int code;
blob - 3dbe5d7d43cf45ec0e7997d43f266c3ce0c9fcbe
blob + 26e15d93b91bc42ee028fa8ecf60a8d1ac4dfdc9
--- include/got_gotconfig.h
+++ include/got_gotconfig.h
@@ -29,3 +29,19 @@ const char *got_gotconfig_get_author(const struct got_
  */
 void got_gotconfig_get_remotes(int *, const struct got_remote_repo **,
     const struct got_gotconfig *);
+
+/*
+ * Obtain the filename of the allowed signers file.
+ * Returns NULL if no configuration file is found or no allowed signers file
+ * is configured.
+ */
+const char *
+got_gotconfig_get_allowed_signers_file(const struct got_gotconfig *);
+
+/*
+ * Obtain the filename of the revoked signers file.
+ * Returns NULL if no configuration file is found or no revoked signers file
+ * is configured.
+ */
+const char *
+got_gotconfig_get_revoked_signers_file(const struct got_gotconfig *);
blob - a8d0318ceaa7152627e8c8718ba039f8517bc3e4
blob + 1cd6f349912d3e03ebbdccfd4beeeb54663af7fb
--- include/got_object.h
+++ include/got_object.h
@@ -351,4 +351,4 @@ const struct got_error *got_object_commit_add_parent(s
 /* Create a new tag object in the repository. */
 const struct got_error *got_object_tag_create(struct got_object_id **,
     const char *, struct got_object_id *, const char *,
-    time_t, const char *, struct got_repository *);
+    time_t, const char *, const char *, struct got_repository *, int verbosity);
blob - /dev/null
blob + e1eb576f115932bb52bb70a663a2facb02643ca9 (mode 644)
--- /dev/null
+++ include/got_tag.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 Josh Rickmar <jrick@zettaport.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+const struct got_error *
+got_tag_apply_unveil(void);
+
+const struct got_error *
+got_tag_sign_ssh(pid_t *, int *, int *, const char *, int);
+
+const char *
+got_tag_message_get_ssh_signature(const char *);
+
+const struct got_error *
+got_tag_verify_ssh(char **, struct got_tag_object *, const char *, const char *,
+    const char *, int);
blob - /dev/null
blob + 618af333cee8c4bcb2b88cb3f4b031788f8f2761 (mode 644)
--- /dev/null
+++ lib/date.c
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2022 Josh Rickmar <jrick@zettaport.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdio.h>
+
+#include "got_date.h"
+
+void
+got_format_gmtoff(char *buf, size_t sz, time_t gmtoff)
+{
+	long long h, m;
+	char sign = '+';
+
+	if (gmtoff < 0) {
+		sign = '-';
+		gmtoff = -gmtoff;
+	}
+
+	h = (long long)gmtoff / 3600;
+	m = ((long long)gmtoff - h*3600) / 60;
+	snprintf(buf, sz, "%c%02lld%02lld", sign, h, m);
+}
blob - 3ffd653ef429fab490d06ba6a953185254e7c117
blob + e88c9483b7d3b719c04835b3500183decbf835d1
--- lib/error.c
+++ lib/error.c
@@ -217,6 +217,8 @@ static const struct got_error got_errors[] = {
 	{ GOT_ERR_PATCH_FAILED, "patch failed to apply" },
 	{ GOT_ERR_FILEIDX_DUP_ENTRY, "duplicate file index entry" },
 	{ GOT_ERR_PIN_PACK, "could not pin pack file" },
+	{ GOT_ERR_BAD_TAG_SIGNATURE, "invalid tag signature" },
+	{ GOT_ERR_CANTVERIFY_TAG_SIGNATURE, "cannot verify signature" },
 };
 
 static struct got_custom_error {
blob - 5e02aa1efeff0dd226e617da410a4663d8376d9a
blob + 39337ed4d9cbe7dfa5939b3f4dcb38793ccddfbd
--- lib/got_lib_gotconfig.h
+++ lib/got_lib_gotconfig.h
@@ -20,6 +20,8 @@ struct got_gotconfig {
 	char *author;
 	int nremotes;
 	struct got_remote_repo *remotes;
+	char *allowed_signers_file;
+	char *revoked_signers_file;
 };
 
 const struct got_error *got_gotconfig_read(struct got_gotconfig **,
blob - 6ffe646e98676cf9a0d19fe3ad27f3e63ab04fcc
blob + dac4ab973b68243e262fd1ae6482fffb6dc2bc57
--- lib/got_lib_privsep.h
+++ lib/got_lib_privsep.h
@@ -172,6 +172,8 @@ enum got_imsg_type {
 	/* Messages related to gotconfig files. */
 	GOT_IMSG_GOTCONFIG_PARSE_REQUEST,
 	GOT_IMSG_GOTCONFIG_AUTHOR_REQUEST,
+	GOT_IMSG_GOTCONFIG_ALLOWEDSIGNERS_REQUEST,
+	GOT_IMSG_GOTCONFIG_REVOKEDSIGNERS_REQUEST,
 	GOT_IMSG_GOTCONFIG_REMOTES_REQUEST,
 	GOT_IMSG_GOTCONFIG_INT_VAL,
 	GOT_IMSG_GOTCONFIG_STR_VAL,
@@ -760,6 +762,10 @@ const struct got_error *got_privsep_recv_gitconfig_rem
 const struct got_error *got_privsep_send_gotconfig_parse_req(struct imsgbuf *,
     int);
 const struct got_error *got_privsep_send_gotconfig_author_req(struct imsgbuf *);
+const struct got_error *got_privsep_send_gotconfig_allowed_signers_req(
+    struct imsgbuf *);
+const struct got_error *got_privsep_send_gotconfig_revoked_signers_req(
+    struct imsgbuf *);
 const struct got_error *got_privsep_send_gotconfig_remotes_req(
     struct imsgbuf *);
 const struct got_error *got_privsep_recv_gotconfig_str(char **,
blob - 5b602c9f5513aee64b98ca608535d5b85280ec42
blob + 7fae8306f7aa444e25b71f0a95f8f151ec324a7f
--- lib/gotconfig.c
+++ lib/gotconfig.c
@@ -101,6 +101,24 @@ got_gotconfig_read(struct got_gotconfig **conf, const 
 	if (err)
 		goto done;
 
+	err = got_privsep_send_gotconfig_allowed_signers_req(ibuf);
+	if (err)
+		goto done;
+
+	err = got_privsep_recv_gotconfig_str(&(*conf)->allowed_signers_file,
+	    ibuf);
+	if (err)
+		goto done;
+
+	err = got_privsep_send_gotconfig_revoked_signers_req(ibuf);
+	if (err)
+		goto done;
+
+	err = got_privsep_recv_gotconfig_str(&(*conf)->revoked_signers_file,
+	    ibuf);
+	if (err)
+		goto done;
+
 	err = got_privsep_send_gotconfig_remotes_req(ibuf);
 	if (err)
 		goto done;
@@ -158,3 +176,15 @@ got_gotconfig_get_remotes(int *nremotes, const struct 
 	*nremotes = conf->nremotes;
 	*remotes = conf->remotes;
 }
+
+const char *
+got_gotconfig_get_allowed_signers_file(const struct got_gotconfig *conf)
+{
+	return conf->allowed_signers_file;
+}
+
+const char *
+got_gotconfig_get_revoked_signers_file(const struct got_gotconfig *conf)
+{
+	return conf->revoked_signers_file;
+}
blob - 5036de1b9a6b491a1fc7c0358a03dcd9574f6cf3
blob + 105e30ab2f4acad2244635c54c23003e67de8a60
--- lib/object_create.c
+++ lib/object_create.c
@@ -17,6 +17,7 @@
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <sys/queue.h>
+#include <sys/wait.h>
 
 #include <ctype.h>
 #include <errno.h>
@@ -35,6 +36,7 @@
 #include "got_repository.h"
 #include "got_opentemp.h"
 #include "got_path.h"
+#include "got_tag.h"
 
 #include "got_lib_sha1.h"
 #include "got_lib_deflate.h"
@@ -45,6 +47,8 @@
 
 #include "got_lib_object_create.h"
 
+#include "buf.h"
+
 #ifndef nitems
 #define nitems(_a) (sizeof(_a) / sizeof((_a)[0]))
 #endif
@@ -608,7 +612,8 @@ done:
 const struct got_error *
 got_object_tag_create(struct got_object_id **id,
     const char *tag_name, struct got_object_id *object_id, const char *tagger,
-    time_t tagger_time, const char *tagmsg, struct got_repository *repo)
+    time_t tagger_time, const char *tagmsg, const char *key_file,
+    struct got_repository *repo, int verbosity)
 {
 	const struct got_error *err = NULL;
 	SHA1_CTX sha1_ctx;
@@ -621,6 +626,7 @@ got_object_tag_create(struct got_object_id **id,
 	char *msg0 = NULL, *msg;
 	const char *obj_type_str;
 	int obj_type;
+	BUF *buf = NULL;
 
 	*id = NULL;
 
@@ -747,6 +753,76 @@ got_object_tag_create(struct got_object_id **id,
 	}
 	tagsize += n;
 
+	if (key_file) {
+		FILE *out;
+		pid_t pid;
+		size_t len;
+		int in_fd, out_fd;
+		int status;
+
+		err = buf_alloc(&buf, 0);
+		if (err)
+			goto done;
+
+		/* signed message */
+		err = buf_puts(&len, buf, obj_str);
+		if (err)
+			goto done;
+		err = buf_puts(&len, buf, type_str);
+		if (err)
+			goto done;
+		err = buf_puts(&len, buf, tag_str);
+		if (err)
+			goto done;
+		err = buf_puts(&len, buf, tagger_str);
+		if (err)
+			goto done;
+		err = buf_putc(buf, '\n');
+		if (err)
+			goto done;
+		err = buf_puts(&len, buf, msg);
+		if (err)
+			goto done;
+		err = buf_putc(buf, '\n');
+		if (err)
+			goto done;
+
+		err = got_tag_sign_ssh(&pid, &in_fd, &out_fd, key_file,
+		    verbosity);
+		if (err)
+			goto done;
+		if (buf_write_fd(buf, in_fd) == -1) {
+			err = got_error_from_errno("write");
+			goto done;
+		}
+		if (close(in_fd) == -1) {
+			err = got_error_from_errno("close");
+			goto done;
+		}
+
+		if (waitpid(pid, &status, 0) == -1) {
+			err = got_error_from_errno("waitpid");
+			goto done;
+		}
+
+		out = fdopen(out_fd, "r");
+		if (out == NULL) {
+			err = got_error_from_errno("fdopen");
+			goto done;
+		}
+		buf_empty(buf);
+		err = buf_load(&buf, out);
+		if (err)
+			goto done;
+		err = buf_putc(buf, '\0');
+		if (err)
+			goto done;
+		if (close(out_fd) == -1) {
+			err = got_error_from_errno("close");
+			goto done;
+		}
+	}
+
 	len = strlen(msg);
 	SHA1Update(&sha1_ctx, msg, len);
 	n = fwrite(msg, 1, len, tagfile);
@@ -764,6 +840,17 @@ got_object_tag_create(struct got_object_id **id,
 	}
 	tagsize += n;
 
+	if (key_file && buf_len(buf) > 0) {
+		len = buf_len(buf);
+		SHA1Update(&sha1_ctx, buf_get(buf), len);
+		n = fwrite(buf_get(buf), 1, len, tagfile);
+		if (n != len) {
+			err = got_ferror(tagfile, GOT_ERR_IO);
+			goto done;
+		}
+		tagsize += n;
+	}
+
 	*id = malloc(sizeof(**id));
 	if (*id == NULL) {
 		err = got_error_from_errno("malloc");
@@ -783,6 +870,8 @@ done:
 	free(header);
 	free(obj_str);
 	free(tagger_str);
+	if (buf)
+		buf_release(buf);
 	if (tagfile && fclose(tagfile) == EOF && err == NULL)
 		err = got_error_from_errno("fclose");
 	if (err) {
blob - c0bdac7221a79c5ec97d1728e862406152d51eb9
blob + 07acf70c2c9103549d8dd9f40e6a173d0bb20401
--- lib/privsep.c
+++ lib/privsep.c
@@ -2365,6 +2365,28 @@ got_privsep_send_gotconfig_author_req(struct imsgbuf *
 }
 
 const struct got_error *
+got_privsep_send_gotconfig_allowed_signers_req(struct imsgbuf *ibuf)
+{
+	if (imsg_compose(ibuf,
+	    GOT_IMSG_GOTCONFIG_ALLOWEDSIGNERS_REQUEST, 0, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("imsg_compose "
+		    "GOTCONFIG_ALLOWEDSIGNERS_REQUEST");
+
+	return flush_imsg(ibuf);
+}
+
+const struct got_error *
+got_privsep_send_gotconfig_revoked_signers_req(struct imsgbuf *ibuf)
+{
+	if (imsg_compose(ibuf,
+	    GOT_IMSG_GOTCONFIG_REVOKEDSIGNERS_REQUEST, 0, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("imsg_compose "
+		    "GOTCONFIG_REVOKEDSIGNERS_REQUEST");
+
+	return flush_imsg(ibuf);
+}
+
+const struct got_error *
 got_privsep_send_gotconfig_remotes_req(struct imsgbuf *ibuf)
 {
 	if (imsg_compose(ibuf,
blob - /dev/null
blob + f7d4feb11e3d09e32eb54239c0dcbbae397cd363 (mode 644)
--- /dev/null
+++ lib/tag.c
@@ -0,0 +1,414 @@
+/*
+ * Copyright (c) 2022 Josh Rickmar <jrick@zettaport.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/queue.h>
+#include <sys/wait.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <string.h>
+#include <err.h>
+#include <assert.h>
+#include <sha1.h>
+
+#include "got_error.h"
+#include "got_date.h"
+#include "got_object.h"
+#include "got_opentemp.h"
+
+#include "got_tag.h"
+
+#include "buf.h"
+
+#ifndef MIN
+#define	MIN(_a,_b) ((_a) < (_b) ? (_a) : (_b))
+#endif
+
+#ifndef nitems
+#define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
+#ifndef GOT_TAG_PATH_SSH_KEYGEN
+#define GOT_TAG_PATH_SSH_KEYGEN	"/usr/bin/ssh-keygen"
+#endif
+
+#ifndef GOT_TAG_PATH_SIGNIFY
+#define GOT_TAG_PATH_SIGNIFY "/usr/bin/signify"
+#endif
+
+const struct got_error *
+got_tag_apply_unveil()
+{
+	if (unveil(GOT_TAG_PATH_SSH_KEYGEN, "x") != 0) {
+		return got_error_from_errno2("unveil",
+		    GOT_TAG_PATH_SSH_KEYGEN);
+	}
+	if (unveil(GOT_TAG_PATH_SIGNIFY, "x") != 0) {
+		return got_error_from_errno2("unveil",
+		    GOT_TAG_PATH_SIGNIFY);
+	}
+
+	return NULL;
+}
+
+const struct got_error *
+got_tag_sign_ssh(pid_t *newpid, int *in_fd, int *out_fd, const char* key_file,
+    int verbosity)
+{
+	const struct got_error *error = NULL;
+	int pid, in_pfd[2], out_pfd[2];
+	const char* argv[11];
+	int i = 0, j;
+
+	*newpid = -1;
+	*in_fd = -1;
+	*out_fd = -1;
+
+	argv[i++] = GOT_TAG_PATH_SSH_KEYGEN;
+	argv[i++] = "-Y";
+	argv[i++] = "sign";
+	argv[i++] = "-f";
+	argv[i++] = key_file;
+	argv[i++] = "-n";
+	argv[i++] = "git";
+	if (verbosity <= 0) {
+		argv[i++] = "-q";
+	} else {
+		/* ssh(1) allows up to 3 "-v" options. */
+		for (j = 0; j < MIN(3, verbosity); j++)
+			argv[i++] = "-v";
+	}
+	argv[i++] = NULL;
+	assert(i <= nitems(argv));
+
+	if (pipe2(in_pfd, 0 /*O_NONBLOCK|O_CLOEXEC*/) == -1)
+		return got_error_from_errno("pipe2");
+	if (pipe2(out_pfd, 0 /*O_NONBLOCK|O_CLOEXEC*/) == -1)
+		return got_error_from_errno("pipe2");
+
+	pid = fork();
+	if (pid == -1) {
+		error = got_error_from_errno("fork");
+		close(in_pfd[0]);
+		close(in_pfd[1]);
+		close(out_pfd[0]);
+		close(out_pfd[1]);
+		return error;
+	} else if (pid == 0) {
+		if (close(in_pfd[1]) == -1)
+			err(1, "close");
+		if (close(out_pfd[1]) == -1)
+			err(1, "close");
+		if (dup2(in_pfd[0], 0) == -1)
+			err(1, "dup2");
+		if (dup2(out_pfd[0], 1) == -1)
+			err(1, "dup2");
+		if (execv(GOT_TAG_PATH_SSH_KEYGEN, (char **const)argv) == -1)
+			err(1, "execv");
+		abort(); /* not reached */
+	}
+	if (close(in_pfd[0]) == -1)
+		return got_error_from_errno("close");
+	if (close(out_pfd[0]) == -1)
+		return got_error_from_errno("close");
+	*newpid = pid;
+	*in_fd = in_pfd[1];
+	*out_fd = out_pfd[1];
+	return NULL;
+}
+
+static char *
+signer_identity(const char *tagger)
+{
+	char *lt, *gt;
+
+        lt = strstr(tagger, " <");
+	gt = strrchr(tagger, '>');
+	if (lt && gt && lt+1 < gt)
+		return strndup(lt+2, gt-lt-2);
+	return NULL;
+}
+
+static const char* BEGIN_SSH_SIG = "-----BEGIN SSH SIGNATURE-----\n";
+static const char* END_SSH_SIG = "-----END SSH SIGNATURE-----\n";
+
+//static const int SIG_KIND_SSH = 1;
+//static const int SIG_KIND_SIGNIFY = 2;
+
+const char *
+got_tag_message_get_ssh_signature(const char *tagmsg)
+{
+	const char *s = tagmsg, *begin = NULL, *end = NULL;
+
+	while ((s = strstr(s, BEGIN_SSH_SIG)) != NULL) {
+		begin = s;
+		s += strlen(BEGIN_SSH_SIG);
+	}
+	if (begin)
+		end = strstr(begin+strlen(BEGIN_SSH_SIG), END_SSH_SIG);
+	if (end == NULL)
+		return NULL;
+	return (end[strlen(END_SSH_SIG)] == '\0') ? begin : NULL;
+}
+
+static const struct got_error *
+got_tag_write_signed_data(BUF *buf, struct got_tag_object *tag,
+    const char *start_sig)
+{
+	const struct got_error *err = NULL;
+	struct got_object_id *id;
+	char *id_str = NULL;
+	char *tagger = NULL;
+	const char *tagmsg;
+	char gmtoff[6];
+	size_t len;
+
+	id = got_object_tag_get_object_id(tag);
+	err = got_object_id_str(&id_str, id);
+	if (err)
+		goto done;
+
+	const char *type_label = NULL;
+	switch (got_object_tag_get_object_type(tag)) {
+	case GOT_OBJ_TYPE_BLOB:
+		type_label = GOT_OBJ_LABEL_BLOB;
+		break;
+	case GOT_OBJ_TYPE_TREE:
+		type_label = GOT_OBJ_LABEL_TREE;
+		break;
+	case GOT_OBJ_TYPE_COMMIT:
+		type_label = GOT_OBJ_LABEL_COMMIT;
+		break;
+	case GOT_OBJ_TYPE_TAG:
+		type_label = GOT_OBJ_LABEL_TAG;
+		break;
+	default:
+		break;
+	}
+	got_format_gmtoff(gmtoff, sizeof(gmtoff),
+	    got_object_tag_get_tagger_gmtoff(tag));
+	if (asprintf(&tagger, "%s %lld %s", got_object_tag_get_tagger(tag),
+	    got_object_tag_get_tagger_time(tag), gmtoff) == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	err = buf_puts(&len, buf, GOT_TAG_LABEL_OBJECT);
+	if (err)
+		goto done;
+	err = buf_puts(&len, buf, id_str);
+	if (err)
+		goto done;
+	err = buf_putc(buf, '\n');
+	if (err)
+		goto done;
+	err = buf_puts(&len, buf, GOT_TAG_LABEL_TYPE);
+	if (err)
+		goto done;
+	err = buf_puts(&len, buf, type_label);
+	if (err)
+		goto done;
+	err = buf_putc(buf, '\n');
+	if (err)
+		goto done;
+	err = buf_puts(&len, buf, GOT_TAG_LABEL_TAG);
+	if (err)
+		goto done;
+	err = buf_puts(&len, buf, got_object_tag_get_name(tag));
+	if (err)
+		goto done;
+	err = buf_putc(buf, '\n');
+	if (err)
+		goto done;
+	err = buf_puts(&len, buf, GOT_TAG_LABEL_TAGGER);
+	if (err)
+		goto done;
+	err = buf_puts(&len, buf, tagger);
+	if (err)
+		goto done;
+	err = buf_puts(&len, buf, "\n");
+	if (err)
+		goto done;
+	tagmsg = got_object_tag_get_message(tag);
+	err = buf_append(&len, buf, tagmsg, start_sig-tagmsg);
+	if (err)
+		goto done;
+
+done:
+	free(id_str);
+	free(tagger);
+	return err;
+}
+
+const struct got_error *
+got_tag_verify_ssh(char **msg, struct got_tag_object *tag,
+    const char *start_sig, const char* allowed_signers, const char* revoked,
+    int verbosity)
+{
+	const struct got_error *error = NULL;
+	const char* argv[17];
+	int pid, status, in_pfd[2], out_pfd[2];
+	char* parsed_identity = NULL;
+	const char *identity;
+	char* tmppath = NULL;
+	FILE *tmpsig, *out = NULL;
+	BUF *buf;
+	int i = 0, j;
+
+	*msg = NULL;
+
+	error = got_opentemp_named(&tmppath, &tmpsig,
+	    GOT_TMPDIR_STR "/got-tagsig");
+	if (error)
+		goto done;
+
+	got_opentemp();
+	if (tmpsig == NULL) {
+		error = got_error_from_errno("got_opentemp");
+		goto done;
+	}
+
+	identity = got_object_tag_get_tagger(tag);
+	parsed_identity = signer_identity(identity);
+	if (parsed_identity != NULL)
+		identity = parsed_identity;
+
+	if (fputs(start_sig, tmpsig) == EOF) {
+		error = got_error_from_errno("fputs");
+		goto done;
+	}
+	if (fflush(tmpsig) == EOF) {
+		error = got_error_from_errno("fflush");
+		goto done;
+	}
+
+	error = buf_alloc(&buf, 0);
+	if (error)
+		goto done;
+	error = got_tag_write_signed_data(buf, tag, start_sig);
+	if (error)
+		goto done;
+
+	argv[i++] = GOT_TAG_PATH_SSH_KEYGEN;
+	argv[i++] = "-Y";
+	argv[i++] = "verify";
+	argv[i++] = "-f";
+	argv[i++] = allowed_signers;
+	argv[i++] = "-I";
+	argv[i++] = identity;
+	argv[i++] = "-n";
+	argv[i++] = "git";
+	argv[i++] = "-s";
+	argv[i++] = tmppath;
+	if (revoked) {
+		argv[i++] = "-r";
+		argv[i++] = revoked;
+	}
+	if (verbosity > 0) {
+		/* ssh(1) allows up to 3 "-v" options. */
+		for (j = 0; j < MIN(3, verbosity); j++)
+			argv[i++] = "-v";
+	}
+	argv[i++] = NULL;
+	assert(i <= nitems(argv));
+
+	if (pipe2(in_pfd, 0 /*O_NONBLOCK|O_CLOEXEC*/) == -1) {
+		error = got_error_from_errno("pipe2");
+		goto done;
+	}
+	if (pipe2(out_pfd, 0 /*O_NONBLOCK|O_CLOEXEC*/) == -1) {
+		error = got_error_from_errno("pipe2");
+		goto done;
+	}
+
+	pid = fork();
+	if (pid == -1) {
+		error = got_error_from_errno("fork");
+		close(in_pfd[0]);
+		close(in_pfd[1]);
+		close(out_pfd[0]);
+		close(out_pfd[1]);
+		return error;
+	} else if (pid == 0) {
+		if (close(in_pfd[1]) == -1)
+			err(1, "close");
+		if (close(out_pfd[1]) == -1)
+			err(1, "close");
+		if (dup2(in_pfd[0], 0) == -1)
+			err(1, "dup2");
+		if (dup2(out_pfd[0], 1) == -1)
+			err(1, "dup2");
+		if (execv(GOT_TAG_PATH_SSH_KEYGEN, (char **const)argv) == -1)
+			err(1, "execv");
+		abort(); /* not reached */
+	}
+	if (close(in_pfd[0]) == -1) {
+		error = got_error_from_errno("close");
+		goto done;
+	}
+	if (close(out_pfd[0]) == -1) {
+		error = got_error_from_errno("close");
+		goto done;
+	}
+	if (buf_write_fd(buf, in_pfd[1]) == -1) {
+		error = got_error_from_errno("write");
+		goto done;
+	}
+	if (close(in_pfd[1]) == -1) {
+		error = got_error_from_errno("close");
+		goto done;
+	}
+	if (waitpid(pid, &status, 0) == -1) {
+		error = got_error_from_errno("waitpid");
+		goto done;
+	}
+	if (!WIFEXITED(status)) {
+		error = got_error(GOT_ERR_BAD_TAG_SIGNATURE);
+		goto done;
+	}
+
+	out = fdopen(out_pfd[1], "r");
+	if (out == NULL) {
+		error = got_error_from_errno("fdopen");
+		goto done;
+	}
+	error = buf_load(&buf, out);
+	if (error)
+		goto done;
+	error = buf_putc(buf, '\0');
+	if (error)
+		goto done;
+	if (close(out_pfd[1]) == -1) {
+		error = got_error_from_errno("close");
+		goto done;
+	}
+	out = NULL;
+	*msg = buf_get(buf);
+
+done:
+	free(parsed_identity);
+	free(tmppath);
+	if (tmpsig && fclose(tmpsig) == EOF && error == NULL)
+		error = got_error_from_errno("fclose");
+	if (out && fclose(out) == EOF && error == NULL)
+		error = got_error_from_errno("fclose");
+	return error;
+}
blob - aa2c97552358174249a7361aba78c785626d6b7f
blob + be0d93073a8d7779e487b6a2d12bad1e6c9721d4
--- libexec/got-read-gotconfig/got-read-gotconfig.c
+++ libexec/got-read-gotconfig/got-read-gotconfig.c
@@ -548,6 +548,24 @@ main(int argc, char *argv[])
 			err = send_gotconfig_str(&ibuf,
 			    gotconfig->author ?  gotconfig->author : "");
 			break;
+		case GOT_IMSG_GOTCONFIG_ALLOWEDSIGNERS_REQUEST:
+			if (gotconfig == NULL) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = send_gotconfig_str(&ibuf,
+			    gotconfig->allowed_signers_file ?
+			        gotconfig->allowed_signers_file : "");
+			break;
+		case GOT_IMSG_GOTCONFIG_REVOKEDSIGNERS_REQUEST:
+			if (gotconfig == NULL) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = send_gotconfig_str(&ibuf,
+			    gotconfig->revoked_signers_file ?
+			        gotconfig->revoked_signers_file : "");
+			break;
 		case GOT_IMSG_GOTCONFIG_REMOTES_REQUEST:
 			if (gotconfig == NULL) {
 				err = got_error(GOT_ERR_PRIVSEP_MSG);
blob - 1ce499222101a45de399bd433825c767df869d91
blob + 504e691250732f7b2baee47695fc1794127b2adb
--- libexec/got-read-gotconfig/gotconfig.h
+++ libexec/got-read-gotconfig/gotconfig.h
@@ -1,4 +1,5 @@
 /*
+ * Copyright (c) 2022 Josh Rickmar <jrick@zettaport.com>
  * Copyright (c) 2020, 2021 Tracey Emery <tracey@openbsd.org>
  * Copyright (c) 2020 Stefan Sperling <stsp@openbsd.org>
  *
@@ -66,6 +67,8 @@ struct gotconfig {
 	char	*author;
 	struct gotconfig_remote_repo_list remotes;
 	int nremotes;
+	char	*allowed_signers_file;
+	char	*revoked_signers_file;
 };
 
 /*
blob - b9a0bd38cabe5d893cbbb04c482578a895a094ed
blob + 85fc623c3bd3ebda367919af6ac405ae817a88fc
--- libexec/got-read-gotconfig/parse.y
+++ libexec/got-read-gotconfig/parse.y
@@ -99,7 +99,8 @@ typedef struct {
 
 %token	ERROR
 %token	REMOTE REPOSITORY SERVER PORT PROTOCOL MIRROR_REFERENCES BRANCH
-%token	AUTHOR FETCH_ALL_BRANCHES REFERENCE FETCH SEND
+%token	AUTHOR ALLOWED_SIGNERS REVOKED_SIGNERS FETCH_ALL_BRANCHES REFERENCE
+%token	FETCH SEND
 %token	<v.string>	STRING
 %token	<v.number>	NUMBER
 %type	<v.number>	boolean portplain
@@ -113,6 +114,7 @@ grammar		: /* empty */
 		| grammar '\n'
 		| grammar author '\n'
 		| grammar remote '\n'
+		| grammar allowed_signers '\n'
 		;
 boolean		: STRING {
 			if (strcasecmp($1, "true") == 0 ||
@@ -306,6 +308,14 @@ author		: AUTHOR STRING {
 			gotconfig.author = $2;
 		}
 		;
+allowed_signers	: ALLOWED_SIGNERS STRING {
+			gotconfig.allowed_signers_file = $2;
+		}
+		;
+revoked_signers	: REVOKED_SIGNERS STRING {
+			gotconfig.revoked_signers_file = $2;
+		}
+		;
 optnl		: '\n' optnl
 		| /* empty */
 		;
@@ -354,6 +364,7 @@ lookup(char *s)
 {
 	/* This has to be sorted always. */
 	static const struct keywords keywords[] = {
+		{"allowed_signers",	ALLOWED_SIGNERS},
 		{"author",		AUTHOR},
 		{"branch",		BRANCH},
 		{"fetch",		FETCH},
@@ -364,6 +375,7 @@ lookup(char *s)
 		{"reference",		REFERENCE},
 		{"remote",		REMOTE},
 		{"repository",		REPOSITORY},
+		{"revoked_signers",	REVOKED_SIGNERS},
 		{"send",		SEND},
 		{"server",		SERVER},
 	};
@@ -791,6 +803,8 @@ gotconfig_free(struct gotconfig *conf)
 	struct gotconfig_remote_repo *remote;
 
 	free(conf->author);
+	free(conf->allowed_signers_file);
+	free(conf->revoked_signers_file);
 	while (!TAILQ_EMPTY(&conf->remotes)) {
 		remote = TAILQ_FIRST(&conf->remotes);
 		TAILQ_REMOVE(&conf->remotes, remote, entry);
blob - 0215869fd1a3678fe92c416a609faf3e875f0a34
blob + e3a920cac7fae89fc7521957115fff5d80f21cdc
--- regress/fetch/Makefile
+++ regress/fetch/Makefile
@@ -4,7 +4,8 @@ PROG = fetch_test
 SRCS = error.c privsep.c reference.c sha1.c object.c object_parse.c path.c \
 	opentemp.c repository.c lockfile.c object_cache.c pack.c inflate.c \
 	deflate.c delta.c delta_cache.c object_idset.c object_create.c \
-	fetch.c gotconfig.c dial.c fetch_test.c bloom.c murmurhash2.c
+	fetch.c gotconfig.c dial.c fetch_test.c bloom.c murmurhash2.c tag.c \
+	buf.c date.c
 
 CPPFLAGS = -I${.CURDIR}/../../include -I${.CURDIR}/../../lib
 LDADD = -lutil -lz -lm
blob - ba79d5e787ada9939dea4f62aae062cea501f845
blob + 1c8c7b124a59736d122d6b3b73c43be42cd29173
--- tog/Makefile
+++ tog/Makefile
@@ -12,7 +12,7 @@ SRCS=		tog.c blame.c commit_graph.c delta.c diff.c \
 		gotconfig.c diff_main.c diff_atomize_text.c \
 		diff_myers.c diff_output.c diff_output_plain.c \
 		diff_output_unidiff.c diff_output_edscript.c \
-		diff_patience.c bloom.c murmurhash2.c
+		diff_patience.c bloom.c murmurhash2.c tag.c date.c
 MAN =		${PROG}.1
 
 CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib