git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[RFC PATCH 2/4] tag: add --hash=sha256 to sign a tree-sha256 header

From
SCScott Chacon <scott@gitbutler.net>
Date
Oct 2, 2026, 08:18 UTC
Message-ID
<20261002081846.25144-3-scott@gitbutler.net>
In-Reply-To
<20261002081846.25144-1-scott@gitbutler.net>

Teach "git tag -s" and "git tag -u" a "--hash=sha256" option that puts the tree-sha256 of the tagged tree in a header after "tagger":

  object <commit>
  type commit
  tag <name>
  tagger <ident>
  tree-sha256 <hex>

Being in the header, it is part of the signed payload, and so the signature now covers the contents of the tagged tree directly.

"--hash=sha256" without signing is an error, as there is nothing to gain from an unsigned digest. It also makes "git tag" create a tag object, so that it isn't silently dropped when making a lightweight tag. "--hash=none" is accepted so that a later patch can let it override a configured default.

---
 Documentation/git-tag.adoc    | 11 ++++-
 builtin/tag.c                 | 29 +++++++++++--
 t/meson.build                 |  1 +
 t/t7032-tree-sha256-signed.sh | 76 +++++++++++++++++++++++++++++++++++
 tree-sha256.c                 |  9 +++++
 tree-sha256.h                 |  6 +++
 6 files changed, 128 insertions(+), 4 deletions(-)
 create mode 100755 t/t7032-tree-sha256-signed.sh
diff --git a/Documentation/git-tag.adoc b/Documentation/git-tag.adoc
index cea3202fdb..8901090a6d 100644
--- a/Documentation/git-tag.adoc
+++ b/Documentation/git-tag.adoc
@@ -9,7 +9,7 @@ git-tag - Create, list, delete or verify tags
 SYNOPSIS
 --------
 [synopsis]
-git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] [-e]
+git tag [-a | -s | -u <key-id>] [--hash=<algorithm>] [-f] [-m <msg> | -F <file>] [-e]
 	[(--trailer <token>[(=|:)<value>])...]
 	<tagname> [<commit> | <object>]
 git tag -d <tagname>...
@@ -84,6 +84,15 @@ OPTIONS
 	`gpg.format` configuration variable. See
 	linkgit:git-config[1].
 
+`--hash=<algorithm>`::
+	When signing, add a `tree-sha256` header holding a SHA-256
+	digest of every file in the tagged object's tree, including the
+	contents of checked-out submodules, so that the signature covers
+	the content directly rather than only its SHA-1 object names.
+	_<algorithm>_ is `sha256`, or `none` (the default).
+	Giving `--hash=sha256` without signing is an error. All
+	submodules must be checked out.
+
 `-f`::
 `--force`::
 	Replace an existing tag with the given name (instead of failing)
diff --git a/builtin/tag.c b/builtin/tag.c
index 06c125b53c..9bc4c946d1 100644
--- a/builtin/tag.c
+++ b/builtin/tag.c
@@ -33,9 +33,10 @@
 #include "write-or-die.h"
 #include "object-file-convert.h"
 #include "trailer.h"
+#include "tree-sha256.h"
 
 static const char * const git_tag_usage[] = {
-	N_("git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] [-e]\n"
+	N_("git tag [-a | -s | -u <key-id>] [--hash=<algorithm>] [-f] [-m <msg> | -F <file>] [-e]\n"
 	   "        [(--trailer <token>[(=|:)<value>])...]\n"
 	   "        <tagname> [<commit> | <object>]"),
 	N_("git tag -d <tagname>..."),
@@ -281,6 +282,7 @@ struct create_tag_options {
 	unsigned int message_given:1;
 	unsigned int use_editor:1;
 	unsigned int sign;
+	unsigned int tree_hash;
 	enum {
 		CLEANUP_NONE,
 		CLEANUP_SPACE,
@@ -316,11 +318,19 @@ static void create_tag(const struct object_id *object, const char *object_ref,
 		    "object %s\n"
 		    "type %s\n"
 		    "tag %s\n"
-		    "tagger %s\n\n",
+		    "tagger %s\n",
 		    oid_to_hex(object),
 		    type_name(type),
 		    tag,
 		    git_committer_info(IDENT_STRICT));
+	if (opt->sign && opt->tree_hash) {
+		strbuf_addstr(&header, TREE_SHA256_HEADER " ");
+		if (tree_sha256_hex(the_repository, object, &header))
+			die(_("unable to compute %s for %s"),
+			    TREE_SHA256_HEADER, object_ref);
+		strbuf_addch(&header, '\n');
+	}
+	strbuf_addch(&header, '\n');
 
 	should_edit = opt->use_editor || !opt->message_given;
 	if (should_edit || trailer_args->nr) {
@@ -468,6 +478,7 @@ int cmd_tag(int argc,
 	int cmdmode = 0, create_tag_object = 0;
 	char *msgfile = NULL;
 	const char *keyid = NULL;
+	const char *hash_arg = NULL;
 	struct msg_arg msg = { .buf = STRBUF_INIT };
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
@@ -503,6 +514,8 @@ int cmd_tag(int argc,
 			   N_("add custom trailer(s)")),
 		OPT_BOOL('e', "edit", &edit_flag, N_("force edit of tag message")),
 		OPT_BOOL('s', "sign", &opt.sign, N_("annotated and GPG-signed tag")),
+		OPT_STRING(0, "hash", &hash_arg, N_("algorithm"),
+			   N_("sign a tree-sha256 header of the tagged tree (sha256 or none)")),
 		OPT_CLEANUP(&cleanup_arg),
 		OPT_STRING('u', "local-user", &keyid, N_("key-id"),
 					N_("use another key to sign the tag")),
@@ -556,6 +569,14 @@ int cmd_tag(int argc,
 
 	argc = parse_options(argc, argv, prefix, options, git_tag_usage, 0);
 
+	if (hash_arg) {
+		int tree_hash = parse_signing_hash(hash_arg);
+		if (tree_hash < 0)
+			die(_("unsupported --hash value '%s' (use 'sha256' or 'none')"),
+			    hash_arg);
+		opt.tree_hash = tree_hash;
+	}
+
 	if (!cmdmode) {
 		if (argc == 0)
 			cmdmode = 'l';
@@ -579,7 +600,7 @@ int cmd_tag(int argc,
 		set_signing_key(keyid);
 	}
 	create_tag_object = (opt.sign || annotate || msg.given || msgfile ||
-			     edit_flag || trailer_args.nr);
+			     edit_flag || trailer_args.nr || opt.tree_hash);
 
 	if ((create_tag_object || force) && (cmdmode != 0))
 		usage_with_options(git_tag_usage, options);
@@ -683,6 +704,8 @@ int cmd_tag(int argc,
 	if (create_tag_object) {
 		if (force_sign_annotate && !annotate)
 			opt.sign = 1;
+		if (opt.tree_hash && !opt.sign)
+			die(_("--hash=%s requires a signed tag (-s or -u)"), hash_arg);
 		path = repo_git_path(the_repository, "TAG_EDITMSG");
 		create_tag(&object, object_ref, tag, &buf, &opt, &prev, &object,
 			   &trailer_args, path);
diff --git a/t/meson.build b/t/meson.build
index 8ff3dbe69d..ff83368847 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -877,6 +877,7 @@ integration_tests = [
   't7012-skip-worktree-writing.sh',
   't7030-verify-tag.sh',
   't7031-verify-tag-signed-ssh.sh',
+  't7032-tree-sha256-signed.sh',
   't7060-wtstatus.sh',
   't7061-wtstatus-ignore.sh',
   't7062-wtstatus-ignorecase.sh',
diff --git a/t/t7032-tree-sha256-signed.sh b/t/t7032-tree-sha256-signed.sh
new file mode 100755
index 0000000000..083f25e665
--- /dev/null
+++ b/t/t7032-tree-sha256-signed.sh
@@ -0,0 +1,76 @@
+#!/bin/sh
+
+test_description='signed tags and commits with a tree-sha256 header'
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+GNUPGHOME_NOT_USED=$GNUPGHOME
+. "$TEST_DIRECTORY/lib-gpg.sh"
+
+# Print the value of the tree-sha256 header of <type> <object>, if any.
+header_of () {
+	git cat-file "$1" "$2" >object &&
+	sed -n "/^$/q; s/^tree-sha256 //p" object
+}
+
+test_expect_success GPGSSH 'setup' '
+	git config --global gpg.format ssh &&
+	git config --global gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+	git config --global user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
+	mkdir dir &&
+	echo one >dir/file &&
+	echo two >file &&
+	git add dir file &&
+	test_tick &&
+	git commit -m initial &&
+	test-tool tree-sha256 HEAD >expect
+'
+
+test_expect_success GPGSSH 'tag -s --hash=sha256 signs a tree-sha256 header' '
+	git tag -s --hash=sha256 -m release v1 &&
+	header_of tag v1 >actual &&
+	test_cmp expect actual &&
+	sed -n "4,5p" object >lines &&
+	test_grep "^tagger " lines &&
+	test_grep "^tree-sha256 " lines &&
+	git tag -v v1 &&
+	git fsck --strict
+'
+
+test_expect_success GPGSSH 'tag -u --hash=sha256 signs a tree-sha256 header' '
+	git tag -u "${GPGSSH_KEY_PRIMARY}" --hash=sha256 -m release v2 &&
+	header_of tag v2 >actual &&
+	test_cmp expect actual &&
+	git tag -v v2
+'
+
+test_expect_success GPGSSH 'tag -s without --hash has no header' '
+	git tag -s -m release v3 &&
+	header_of tag v3 >actual &&
+	test_must_be_empty actual &&
+	git tag -s --hash=none -m release v4 &&
+	header_of tag v4 >actual &&
+	test_must_be_empty actual
+'
+
+test_expect_success GPGSSH 'tag --hash=sha256 requires signing' '
+	test_must_fail git tag --hash=sha256 -m release v5 2>err &&
+	test_grep "requires a signed tag" err &&
+	test_must_fail git tag --hash=sha256 v5 2>err &&
+	test_grep "requires a signed tag" err &&
+	test_must_fail git rev-parse --verify v5
+'
+
+test_expect_success GPGSSH 'tag --hash rejects unknown algorithms' '
+	test_must_fail git tag -s --hash=md5 -m release v5 2>err &&
+	test_grep "unsupported --hash value" err &&
+	test_must_fail git rev-parse --verify v5
+'
+
+test_expect_success GPGSSH 'tag --hash=sha256 needs an object with a tree' '
+	test_must_fail git tag -s --hash=sha256 -m blob v5 HEAD:file &&
+	test_must_fail git rev-parse --verify v5
+'
+
+test_done
diff --git a/tree-sha256.c b/tree-sha256.c
index fb58962232..90f0306521 100644
--- a/tree-sha256.c
+++ b/tree-sha256.c
@@ -236,3 +236,12 @@ int tree_sha256_hex(struct repository *r, const struct object_id *oid,
 	oid_array_clear(&chain);
 	return ret;
 }
+
+int parse_signing_hash(const char *value)
+{
+	if (!strcasecmp(value, "sha256"))
+		return 1;
+	if (!strcasecmp(value, "none"))
+		return 0;
+	return -1;
+}
diff --git a/tree-sha256.h b/tree-sha256.h
index 6d3e5018aa..dc070129ea 100644
--- a/tree-sha256.h
+++ b/tree-sha256.h
@@ -27,4 +27,10 @@ struct strbuf;
 int tree_sha256_hex(struct repository *r, const struct object_id *oid,
 		    struct strbuf *hex);
 
+/*
+ * Parse the value of a --hash=<algorithm> option. Returns 1 for
+ * "sha256", 0 for "none", and -1 for anything else.
+ */
+int parse_signing_hash(const char *value);
+
 #endif /* TREE_SHA256_H */
-- 
2.50.1 (Apple Git-155)
Previous: Junio C HamanoNext: Junio C Hamano
Message 4 of 22 in “sign a SHA-256 digest of the tree in commits and tags”
  1. 0/4 sign a SHA-256 digest of the tree in commits and tagsScott Chacon, Oct 2, 2026
  2. 1/4 tree-sha256: hash the contents of a tree with SHA-256Scott Chacon, Oct 2, 2026
  3. Junio C HamanoOct 2, 2026
  4. 2/4 tag: add --hash=sha256 to sign a tree-sha256 headerScott Chacon, Oct 2, 2026
  5. Junio C HamanoOct 2, 2026
  6. 3/4 commit: add --hash=sha256 to sign a tree-sha256 headerScott Chacon, Oct 2, 2026
  7. 4/4 gpg: add gpg.treeHash to sign a tree-sha256 header by defaultScott Chacon, Oct 2, 2026
  8. Junio C HamanoOct 2, 2026
  9. brian m. carlsonOct 2, 2026
  10. Scott ChaconOct 5, 2026
  11. Patrick SteinhardtOct 5, 2026
  12. Scott ChaconOct 5, 2026
  13. brian m. carlsonOct 5, 2026
  14. Johannes SchindelinOct 6, 2026
  15. Kristoffer HaugsbakkOct 6, 2026
  16. brian m. carlsonOct 6, 2026
  17. Junio C HamanoOct 6, 2026
  18. brian m. carlsonOct 6, 2026
  19. Christian CouderOct 6, 2026
  20. brian m. carlsonOct 6, 2026
  21. Christian CouderOct 7, 2026
  22. brian m. carlsonOct 7, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.