Commit Diff


commit - 6c77e0337609d941281f017b3e0229ae45a35b7a
commit + 4d5ee9564a9e46a1f634f619833c62f636cfbdc1
blob - 8dfd844f3da472b6ed040a62acaf85403cbc07ea
blob + 1b45b53a4efff9977dcd3c2e2e33c499adc94533
--- 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 sigs.c date.c
 
 MAN =		${PROG}.1 got-worktree.5 git-repository.5 got.conf.5
 
blob - 955f25359f4f5564010ae23da14cd2ad5b1c4b05
blob + 6feb1c3bbdd91673897061cb3ccbd9c80799d101
--- got/got.c
+++ got/got.c
@@ -58,6 +58,8 @@
 #include "got_gotconfig.h"
 #include "got_dial.h"
 #include "got_patch.h"
+#include "got_sigs.h"
+#include "got_date.h"
 
 #ifndef nitems
 #define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
@@ -684,6 +686,76 @@ get_author(char **author, struct got_repository *repo,
 		*author = NULL;
 	}
 	return err;
+}
+
+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 *
@@ -6837,7 +6909,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);
 }
 
@@ -6917,12 +6990,14 @@ 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;
 	struct got_reflist_entry *re;
 	char *wanted_refname = NULL;
+	int bad_sigs = 0;
 
 	TAILQ_INIT(&refs);
 
@@ -6946,7 +7021,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;
@@ -6962,8 +7038,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)
@@ -6996,6 +7070,22 @@ list_tags(struct got_repository *repo, const char *tag
 			if (err)
 				break;
 		}
+
+		if (verify_tags) {
+			ssh_sig = got_sigs_get_tagmsg_ssh_signature(
+			    got_object_tag_get_message(tag));
+			if (ssh_sig && allowed_signers == NULL) {
+				err = got_error_msg(
+				    GOT_ERR_VERIFY_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)
@@ -7025,6 +7115,19 @@ list_tags(struct got_repository *repo, const char *tag
 			}
 		}
 		free(id_str);
+
+		if (ssh_sig) {
+			err = got_sigs_verify_tag_ssh(&sig_msg, tag, ssh_sig,
+				allowed_signers, revoked_signers, verbosity);
+			if (err && err->code == GOT_ERR_BAD_TAG_SIGNATURE)
+				bad_sigs = 1;
+			else 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)
@@ -7050,6 +7153,9 @@ list_tags(struct got_repository *repo, const char *tag
 done:
 	got_ref_list_free(&refs);
 	free(wanted_refname);
+
+	if (err == NULL && bad_sigs)
+		err = got_error(GOT_ERR_BAD_TAG_SIGNATURE);
 	return err;
 }
 
@@ -7098,9 +7204,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;
@@ -7110,7 +7213,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;
@@ -7166,10 +7270,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_sigs_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;
@@ -7223,11 +7335,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;
@@ -7245,6 +7359,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 */
@@ -7305,26 +7431,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_sigs_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)
@@ -7344,6 +7484,11 @@ cmd_tag(int argc, char *argv[])
 		}
 
 		if (tagmsg) {
+			if (signer_id) {
+				error = got_sigs_apply_unveil();
+				if (error)
+					goto done;
+			}
 			error = apply_unveil(got_repo_get_path(repo), 0, NULL);
 			if (error)
 				goto done;
@@ -7368,7 +7513,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) {
@@ -7389,6 +7535,8 @@ done:
 	free(gitconfig_path);
 	free(commit_id_str);
 	free(tagger);
+	free(allowed_signers);
+	free(revoked_signers);
 	return error;
 }
 
@@ -12419,23 +12567,7 @@ cat_tree(struct got_object_id *id, struct got_reposito
 	got_object_tree_close(tree);
 	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)
 {
@@ -12467,14 +12599,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_date_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_date_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),
@@ -12533,7 +12665,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_date_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 + bf9729a9216142455edfd253fb05cd98c0b4b1f1
--- 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 \
+		sigs.c buf.c date.c
 MAN =		${PROG}.1
 
 CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib
blob - aa54a17419d407bb09bbc7b00af392c74aa8801f
blob + 948b9b8fc5270878f0eb2aa61a579197663e4827
--- gotweb/Makefile
+++ gotweb/Makefile
@@ -15,7 +15,7 @@ SRCS =		gotweb.c parse.y blame.c commit_graph.c delta.
 		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
+		bloom.c murmurhash2.c sigs.c date.c
 MAN =		${PROG}.conf.5 ${PROG}.8
 
 CPPFLAGS +=	-I${.CURDIR}/../include -I${.CURDIR}/../lib -I${.CURDIR} \
blob - /dev/null
blob + b005c2c948e0b4b35147550b1b23fef240ddf8b4 (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_date_format_gmtoff(char *, size_t, time_t);
blob - 22a9264b9f8d0c0b20b48895dd8ea59708e61d48
blob + 4bfaed588e2d81e1b0578a939dd96553de6bc11b
--- 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_VERIFY_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 + 204a6265963d6dcbf4d6f3de13f4bdbafaafc6fa (mode 644)
--- /dev/null
+++ include/got_sigs.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_sigs_apply_unveil(void);
+
+const struct got_error *
+got_sigs_sign_tag_ssh(pid_t *, int *, int *, const char *, int);
+
+const char *
+got_sigs_get_tagmsg_ssh_signature(const char *);
+
+const struct got_error *
+got_sigs_verify_tag_ssh(char **, struct got_tag_object *, const char *,
+    const char *, const char *, int);
blob - /dev/null
blob + 815b291ce868d18136ce8f45fa2f890b6f6c08f9 (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_date_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 + 3c092e61bab70845c184eb10f359eb6df3ee01ce
--- 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_VERIFY_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 + 8f33d6ba0309e1d8f43ef3b0c35b661bd6045211
--- 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_sigs.h"
 
 #include "got_lib_sha1.h"
 #include "got_lib_deflate.h"
@@ -44,6 +46,8 @@
 #include "got_lib_lockfile.h"
 
 #include "got_lib_object_create.h"
+
+#include "buf.h"
 
 #ifndef nitems
 #define nitems(_a) (sizeof(_a) / sizeof((_a)[0]))
@@ -608,19 +612,21 @@ 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;
 	char *header = NULL;
 	char *tag_str = NULL, *tagger_str = NULL;
 	char *id_str = NULL, *obj_str = NULL, *type_str = NULL;
-	size_t headerlen, len = 0, n;
+	size_t headerlen, len = 0, sig_len = 0, n;
 	FILE *tagfile = NULL;
 	off_t tagsize = 0;
 	char *msg0 = NULL, *msg;
 	const char *obj_type_str;
 	int obj_type;
+	BUF *buf = NULL;
 
 	*id = NULL;
 
@@ -681,9 +687,79 @@ got_object_tag_create(struct got_object_id **id,
 	while (isspace((unsigned char)msg[0]))
 		msg++;
 
-	len = strlen(obj_str) + strlen(type_str) + strlen(tag_str) +
-	    strlen(tagger_str) + 1 + strlen(msg) + 1;
+	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_sigs_sign_tag_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;
+		sig_len = buf_len(buf) + 1;
+		err = buf_putc(buf, '\0');
+		if (err)
+			goto done;
+		if (close(out_fd) == -1) {
+			err = got_error_from_errno("close");
+			goto done;
+		}
+	}
+
+	len = strlen(obj_str) + strlen(type_str) + strlen(tag_str) +
+	    strlen(tagger_str) + 1 + strlen(msg) + 1 + sig_len;
 	if (asprintf(&header, "%s %zd", GOT_OBJ_LABEL_TAG, len) == -1) {
 		err = got_error_from_errno("asprintf");
 		goto done;
@@ -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 - 5655e967b5d1a0320fe3e1ef6184b52da9adc4d8
blob + 9a16d647b870eb31f58c9d131d1174c60e7d5eb1
--- lib/privsep.c
+++ lib/privsep.c
@@ -2361,6 +2361,28 @@ got_privsep_send_gotconfig_author_req(struct imsgbuf *
 	    GOT_IMSG_GOTCONFIG_AUTHOR_REQUEST, 0, 0, -1, NULL, 0) == -1)
 		return got_error_from_errno("imsg_compose "
 		    "GOTCONFIG_AUTHOR_REQUEST");
+
+	return flush_imsg(ibuf);
+}
+
+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);
 }
blob - /dev/null
blob + 0b04e3f959ed7aab07534ee9bb5cb4a63e0dd93f (mode 644)
--- /dev/null
+++ lib/sigs.c
@@ -0,0 +1,407 @@
+/*
+ * 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_sigs.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_sigs_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_sigs_sign_tag_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) == -1)
+		return got_error_from_errno("pipe2");
+	if (pipe2(out_pfd, 0) == -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";
+
+const char *
+got_sigs_get_tagmsg_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_date_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_sigs_verify_tag_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;
+
+	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) == -1) {
+		error = got_error_from_errno("pipe2");
+		goto done;
+	}
+	if (pipe2(out_pfd, 0) == -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);
+	if (WEXITSTATUS(status) != 0)
+		error = got_error(GOT_ERR_BAD_TAG_SIGNATURE);
+
+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
@@ -547,6 +547,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) {
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 ||
@@ -304,6 +306,14 @@ remote		: REMOTE STRING {
 		;
 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
@@ -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 - 53325e40ea937187e8814d7b18dd3a6a2f5c40f5
blob + b39af2be74c1e13b37e5bb89219e62eed8046e23
--- regress/cmdline/tag.sh
+++ regress/cmdline/tag.sh
@@ -256,8 +256,169 @@ test_tag_list_lightweight() {
 	fi
 	test_done "$testroot" "$ret"
 }
+
+test_tag_create_ssh_signed() {
+	local testroot=`test_init tag_create`
+	local commit_id=`git_show_head $testroot/repo`
+	local tag=1.0.0
+	local tag2=2.0.0
+
+	ssh-keygen -q -N '' -t ed25519 -f $testroot/id_ed25519
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "ssh-keygen failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	touch $testroot/allowed_signers
+	echo "allowed_signers \"$testroot/allowed_signers\"" > \
+		$testroot/repo/.git/got.conf
+
+	# Create a signed tag based on repository's HEAD reference
+	got tag -s $testroot/id_ed25519 -m 'test' -r $testroot/repo -c HEAD \
+		$tag > $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got tag command failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	tag_id=`got ref -r $testroot/repo -l \
+		| grep "^refs/tags/$tag" | tr -d ' ' | cut -d: -f2`
+	echo "Created tag $tag_id" > $testroot/stdout.expected
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# Ensure validation fails when the key is not allowed
+	echo "signature: Could not verify signature." > \
+		$testroot/stdout.expected
+	VERIFY_STDOUT=$(got tag -r $testroot/repo -V $tag 2> $testroot/stderr)
+	ret=$?
+	echo "$VERIFY_STDOUT" | grep '^signature: ' > $testroot/stdout
+	if [ $ret -eq 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "1"
+		return 1
+	fi
 
+	GOOD_SIG='Good "git" signature for flan_hacker@openbsd.org with ED25519 key SHA256:'
+
+	# Validate the signature with the key allowed
+	echo -n 'flan_hacker@openbsd.org ' > $testroot/allowed_signers
+	cat $testroot/id_ed25519.pub >> $testroot/allowed_signers
+	GOT_STDOUT=$(got tag -r $testroot/repo -V $tag 2> $testroot/stderr)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got tag command failed unexpectedly"
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	
+	if ! echo "$GOT_STDOUT" | grep -q "^signature: $GOOD_SIG"; then
+		echo "got tag command failed to validate signature"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	# Ensure that Git recognizes and verifies the tag Got has created
+	(cd $testroot/repo && git checkout -q $tag)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "git checkout command failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	(cd $testroot/repo && git config --local gpg.ssh.allowedSignersFile \
+		$testroot/allowed_signers)
+	GIT_STDERR=$(cd $testroot/repo && git tag -v $tag 2>&1 1>/dev/null)
+	if ! echo "$GIT_STDERR" | grep -q "^$GOOD_SIG"; then
+		echo "git tag command failed to validate signature"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	# Ensure Got recognizes the new tag
+	got checkout -c $tag $testroot/repo $testroot/wt >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout command failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# Create a tag based on implied worktree HEAD ref
+	(cd $testroot/wt && got tag -m 'test' $tag2 > $testroot/stdout)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	tag_id2=`got ref -r $testroot/repo -l \
+		| grep "^refs/tags/$tag2" | tr -d ' ' | cut -d: -f2`
+	echo "Created tag $tag_id2" > $testroot/stdout.expected
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/repo && git checkout -q $tag2)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "git checkout command failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# Attempt to create a tag pointing at a non-commit
+	local tree_id=`git_show_tree $testroot/repo`
+	(cd $testroot/wt && got tag -m 'test' -c $tree_id foobar \
+		2> $testroot/stderr)
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "git tag command succeeded unexpectedly"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "got: commit $tree_id: object not found" \
+		> $testroot/stderr.expected
+	cmp -s $testroot/stderr $testroot/stderr.expected
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	got ref -r $testroot/repo -l > $testroot/stdout
+	echo "HEAD: $commit_id" > $testroot/stdout.expected
+	echo -n "refs/got/worktree/base-" >> $testroot/stdout.expected
+	cat $testroot/wt/.got/uuid | tr -d '\n' >> $testroot/stdout.expected
+	echo ": $commit_id" >> $testroot/stdout.expected
+	echo "refs/heads/master: $commit_id" >> $testroot/stdout.expected
+	echo "refs/tags/$tag: $tag_id" >> $testroot/stdout.expected
+	echo "refs/tags/$tag2: $tag_id2" >> $testroot/stdout.expected
+	cmp -s $testroot/stdout $testroot/stdout.expected
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_tag_create
 run_test test_tag_list
 run_test test_tag_list_lightweight
+run_test test_tag_create_ssh_signed
blob - 0215869fd1a3678fe92c416a609faf3e875f0a34
blob + f835a2398bf16bf81771722cceeaea81aaa9423b
--- 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 sigs.c \
+	buf.c date.c
 
 CPPFLAGS = -I${.CURDIR}/../../include -I${.CURDIR}/../../lib
 LDADD = -lutil -lz -lm
blob - ba79d5e787ada9939dea4f62aae062cea501f845
blob + 7379d7e77fb9e190eb44211cdf722939696a6cbe
--- 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 sigs.c date.c
 MAN =		${PROG}.1
 
 CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib