commit: write commits for both hashes

When we write a commit, we include data that is specific to the hash algorithm, such as parents and the root tree. In order to write both a SHA-1 commit and a SHA-256 version, we need to convert between them. However, a straightforward conversion isn't necessarily what we want. When we sign a commit, we sign its data, so if we create a commit for SHA-256 and then write a SHA-1 version, we'll still have only signed the SHA-256 data. While this is valid, it would be better to sign both forms of data so people using SHA-1 can verify the signatures as well. Consequently, we don't want to use the standard mapping that occurs when we write an object. Instead, let's move most of the writing of the commit into a separate function which is agnostic of the hash algorithm and which simply writes into a buffer and specify both versions of the object ourselves. We can then call this function twice: once with the SHA-256 contents, and if SHA-1 is enabled, once with the SHA-1 contents. If we're signing the commit, we then sign both versions and append both signatures to both buffers. To produce a consistent hash, we always append the signatures in the order in which Git implemented them: first SHA-1, then SHA-256. In order to make this signing code work, we split the commit signing code into two functions, one which signs the buffer, and one which appends the signature. Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net> Signed-off-by: Eric W. Biederman <ebiederm@xmission.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

brian m. carlson committed Oct 1, 2023 at 21:40 UTC 6206089cbd0b1cb30a017ec904567f040ab4cea0
1 file changed +136 -45
commit.c
+136 -45
@@ -28,6 +28,7 @@
28 #include "shallow.h"
29 #include "tree.h"
30 #include "hook.h"
31 +#include "object-file-convert.h"
32
33 static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);
34
@@ -1100,12 +1101,11 @@ static const char *gpg_sig_headers[] = {
1101 "gpgsig-sha256",
1102 };
1103
1103 -int sign_with_header(struct strbuf *buf, const char *keyid)
1104 +static int add_commit_signature(struct strbuf *buf, struct strbuf *sig, const struct git_hash_algo *algo)
1105 {
1105 - struct strbuf sig = STRBUF_INIT;
1106 int inspos, copypos;
1107 const char *eoh;
1108 - const char *gpg_sig_header = gpg_sig_headers[hash_algo_by_ptr(the_hash_algo)];
1108 + const char *gpg_sig_header = gpg_sig_headers[hash_algo_by_ptr(algo)];
1109 int gpg_sig_header_len = strlen(gpg_sig_header);
1110
1111 /* find the end of the header */
@@ -1115,15 +1115,8 @@ int sign_with_header(struct strbuf *buf, const char *keyid)
1115 else
1116 inspos = eoh - buf->buf + 1;
1117
1118 - if (!keyid || !*keyid)
1119 - keyid = get_signing_key();
1120 - if (sign_buffer(buf, &sig, keyid)) {
1121 - strbuf_release(&sig);
1122 - return -1;
1123 - }
1124 -
1125 - for (copypos = 0; sig.buf[copypos]; ) {
1126 - const char *bol = sig.buf + copypos;
1118 + for (copypos = 0; sig->buf[copypos]; ) {
1119 + const char *bol = sig->buf + copypos;
1120 const char *eol = strchrnul(bol, '\n');
1121 int len = (eol - bol) + !!*eol;
1122
@@ -1136,11 +1129,17 @@ int sign_with_header(struct strbuf *buf, const char *keyid)
1129 inspos += len;
1130 copypos += len;
1131 }
1139 - strbuf_release(&sig);
1132 return 0;
1133 }
1134
1143 -
1135 +static int sign_commit_to_strbuf(struct strbuf *sig, struct strbuf *buf, const char *keyid)
1136 +{
1137 + if (!keyid || !*keyid)
1138 + keyid = get_signing_key();
1139 + if (sign_buffer(buf, sig, keyid))
1140 + return -1;
1141 + return 0;
1142 +}
1143
1144 int parse_signed_commit(const struct commit *commit,
1145 struct strbuf *payload, struct strbuf *signature,
@@ -1599,70 +1598,162 @@ N_("Warning: commit message did not conform to UTF-8.\n"
1598 "You may want to amend it after fixing the message, or set the config\n"
1599 "variable i18n.commitEncoding to the encoding your project uses.\n");
1600
1602 -int commit_tree_extended(const char *msg, size_t msg_len,
1603 - const struct object_id *tree,
1604 - struct commit_list *parents, struct object_id *ret,
1605 - const char *author, const char *committer,
1606 - const char *sign_commit,
1607 - struct commit_extra_header *extra)
1601 +static void write_commit_tree(struct strbuf *buffer, const char *msg, size_t msg_len,
1602 + const struct object_id *tree,
1603 + const struct object_id *parents, size_t parents_len,
1604 + const char *author, const char *committer,
1605 + struct commit_extra_header *extra)
1606 {
1609 - int result;
1607 int encoding_is_utf8;
1611 - struct strbuf buffer;
1612 -
1613 - assert_oid_type(tree, OBJ_TREE);
1614 -
1615 - if (memchr(msg, '\0', msg_len))
1616 - return error("a NUL byte in commit log message not allowed.");
1608 + size_t i;
1609
1610 /* Not having i18n.commitencoding is the same as having utf-8 */
1611 encoding_is_utf8 = is_encoding_utf8(git_commit_encoding);
1612
1621 - strbuf_init(&buffer, 8192); /* should avoid reallocs for the headers */
1622 - strbuf_addf(&buffer, "tree %s\n", oid_to_hex(tree));
1613 + strbuf_grow(buffer, 8192); /* should avoid reallocs for the headers */
1614 + strbuf_addf(buffer, "tree %s\n", oid_to_hex(tree));
1615
1616 /*
1617 * NOTE! This ordering means that the same exact tree merged with a
1618 * different order of parents will be a _different_ changeset even
1619 * if everything else stays the same.
1620 */
1629 - while (parents) {
1630 - struct commit *parent = pop_commit(&parents);
1631 - strbuf_addf(&buffer, "parent %s\n",
1632 - oid_to_hex(&parent->object.oid));
1633 - }
1621 + for (i = 0; i < parents_len; i++)
1622 + strbuf_addf(buffer, "parent %s\n", oid_to_hex(&parents[i]));
1623
1624 /* Person/date information */
1625 if (!author)
1626 author = git_author_info(IDENT_STRICT);
1638 - strbuf_addf(&buffer, "author %s\n", author);
1627 + strbuf_addf(buffer, "author %s\n", author);
1628 if (!committer)
1629 committer = git_committer_info(IDENT_STRICT);
1641 - strbuf_addf(&buffer, "committer %s\n", committer);
1630 + strbuf_addf(buffer, "committer %s\n", committer);
1631 if (!encoding_is_utf8)
1643 - strbuf_addf(&buffer, "encoding %s\n", git_commit_encoding);
1632 + strbuf_addf(buffer, "encoding %s\n", git_commit_encoding);
1633
1634 while (extra) {
1646 - add_extra_header(&buffer, extra);
1635 + add_extra_header(buffer, extra);
1636 extra = extra->next;
1637 }
1649 - strbuf_addch(&buffer, '\n');
1638 + strbuf_addch(buffer, '\n');
1639
1640 /* And add the comment */
1652 - strbuf_add(&buffer, msg, msg_len);
1641 + strbuf_add(buffer, msg, msg_len);
1642 +}
1643
1654 - /* And check the encoding */
1655 - if (encoding_is_utf8 && !verify_utf8(&buffer))
1656 - fprintf(stderr, _(commit_utf8_warn));
1644 +int commit_tree_extended(const char *msg, size_t msg_len,
1645 + const struct object_id *tree,
1646 + struct commit_list *parents, struct object_id *ret,
1647 + const char *author, const char *committer,
1648 + const char *sign_commit,
1649 + struct commit_extra_header *extra)
1650 +{
1651 + struct repository *r = the_repository;
1652 + int result = 0;
1653 + int encoding_is_utf8;
1654 + struct strbuf buffer = STRBUF_INIT, compat_buffer = STRBUF_INIT;
1655 + struct strbuf sig = STRBUF_INIT, compat_sig = STRBUF_INIT;
1656 + struct object_id *parent_buf = NULL, *compat_oid = NULL;
1657 + struct object_id compat_oid_buf;
1658 + size_t i, nparents;
1659 +
1660 + /* Not having i18n.commitencoding is the same as having utf-8 */
1661 + encoding_is_utf8 = is_encoding_utf8(git_commit_encoding);
1662 +
1663 + assert_oid_type(tree, OBJ_TREE);
1664 +
1665 + if (memchr(msg, '\0', msg_len))
1666 + return error("a NUL byte in commit log message not allowed.");
1667
1658 - if (sign_commit && sign_with_header(&buffer, sign_commit)) {
1668 + nparents = commit_list_count(parents);
1669 + CALLOC_ARRAY(parent_buf, nparents);
1670 + i = 0;
1671 + while (parents) {
1672 + struct commit *parent = pop_commit(&parents);
1673 + oidcpy(&parent_buf[i++], &parent->object.oid);
1674 + }
1675 +
1676 + write_commit_tree(&buffer, msg, msg_len, tree, parent_buf, nparents, author, committer, extra);
1677 + if (sign_commit && sign_commit_to_strbuf(&sig, &buffer, sign_commit)) {
1678 result = -1;
1679 goto out;
1680 }
1681 + if (r->compat_hash_algo) {
1682 + struct object_id mapped_tree;
1683 + struct object_id *mapped_parents;
1684 +
1685 + CALLOC_ARRAY(mapped_parents, nparents);
1686 +
1687 + if (repo_oid_to_algop(r, tree, r->compat_hash_algo, &mapped_tree)) {
1688 + result = -1;
1689 + free(mapped_parents);
1690 + goto out;
1691 + }
1692 + for (i = 0; i < nparents; i++)
1693 + if (repo_oid_to_algop(r, &parent_buf[i], r->compat_hash_algo, &mapped_parents[i])) {
1694 + result = -1;
1695 + free(mapped_parents);
1696 + goto out;
1697 + }
1698 + write_commit_tree(&compat_buffer, msg, msg_len, &mapped_tree,
1699 + mapped_parents, nparents, author, committer, extra);
1700 + free(mapped_parents);
1701 +
1702 + if (sign_commit && sign_commit_to_strbuf(&compat_sig, &compat_buffer, sign_commit)) {
1703 + result = -1;
1704 + goto out;
1705 + }
1706 + }
1707 +
1708 + if (sign_commit) {
1709 + struct sig_pairs {
1710 + struct strbuf *sig;
1711 + const struct git_hash_algo *algo;
1712 + } bufs [2] = {
1713 + { &compat_sig, r->compat_hash_algo },
1714 + { &sig, r->hash_algo },
1715 + };
1716 + int i;
1717 +
1718 + /*
1719 + * We write algorithms in the order they were implemented in
1720 + * Git to produce a stable hash when multiple algorithms are
1721 + * used.
1722 + */
1723 + if (r->compat_hash_algo && hash_algo_by_ptr(bufs[0].algo) > hash_algo_by_ptr(bufs[1].algo))
1724 + SWAP(bufs[0], bufs[1]);
1725 +
1726 + /*
1727 + * We traverse each algorithm in order, and apply the signature
1728 + * to each buffer.
1729 + */
1730 + for (i = 0; i < ARRAY_SIZE(bufs); i++) {
1731 + if (!bufs[i].algo)
1732 + continue;
1733 + add_commit_signature(&buffer, bufs[i].sig, bufs[i].algo);
1734 + if (r->compat_hash_algo)
1735 + add_commit_signature(&compat_buffer, bufs[i].sig, bufs[i].algo);
1736 + }
1737 + }
1738
1663 - result = write_object_file(buffer.buf, buffer.len, OBJ_COMMIT, ret);
1739 + /* And check the encoding. */
1740 + if (encoding_is_utf8 && (!verify_utf8(&buffer) || !verify_utf8(&compat_buffer)))
1741 + fprintf(stderr, _(commit_utf8_warn));
1742 +
1743 + if (r->compat_hash_algo) {
1744 + hash_object_file(r->compat_hash_algo, compat_buffer.buf, compat_buffer.len,
1745 + OBJ_COMMIT, &compat_oid_buf);
1746 + compat_oid = &compat_oid_buf;
1747 + }
1748 +
1749 + result = write_object_file_flags(buffer.buf, buffer.len, OBJ_COMMIT,
1750 + ret, compat_oid, 0);
1751 out:
1752 + free(parent_buf);
1753 strbuf_release(&buffer);
1754 + strbuf_release(&compat_buffer);
1755 + strbuf_release(&sig);
1756 + strbuf_release(&compat_sig);
1757 return result;
1758 }
1759