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

From:
Tracey Emery <tracey@traceyemery.net>
Subject:
Re: Tag signing with SSH signatures
To:
Josh Rickmar <openbsd+lists@zettaport.com>
Cc:
gameoftrees@openbsd.org
Date:
Thu, 30 Jun 2022 06:05:00 -0600

Download raw body.

Thread
On Thu, Jun 30, 2022 at 07:37:22AM -0400, Josh Rickmar wrote:
> 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-----
> 

See below. And you forgot to fix gotweb.

> 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];

Same here.

> +	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;

did you miss something here?

> +
> +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;
> +	}
this doesn't look right. i don't think it should exist
----------------------
> +
> +	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
> 

-- 

Tracey Emery