18

I'm trying to figure out how to sign/verify commits by hand, but I can't figure out what data is being signed to create the signature. In other words, I can't figure out what <data> in gpg --verify <commit-sig> <data> needs to be.

Here's the relevant bit of git's source code: https://github.com/git/git/blob/master/commit.c#L1047-L1231 but I'm also new to C.


Here's some example data:

In a fresh git repo, I create a file ledger.txt and commit it with a signed commit:

git config --global user.signingkey 7E482429
git init
echo "EAC5-531F-38E8-9670-81AE-4E77-C7AA-5FC3-7E48-2429 1\n" > ledger.txt
git add ledger.txt
git commit -m "Initial commit" --gpg-sign=7E482429

And here it is in the log:

git log --show-signature

    commit 876793da21833b5b8197b08462523fd6aad3e5ba
    gpg: Signature made Fri May  9 20:01:55 2014 CDT using RSA key ID 7E482429
    gpg: Good signature from "Dan Neumann <danrodneu@gmail.com>"
    Author: Dan Neumann <danrodneu@gmail.com>
    Date:   Fri May 9 20:01:55 2014 -0500

        Initial commit

Here's the pretty-printed commit object (which lives in .git/objects/87/6793da21833b5b8197b08462523fd6aad3e5ba):

git cat-file -p 876793da21833b5b8197b08462523fd6aad3e5ba

tree 70e7c184c3a89c749174b4987830c287fd78952d
author Dan Neumann <danrodneu@gmail.com> 1399683715 -0500
committer Dan Neumann <danrodneu@gmail.com> 1399683715 -0500
gpgsig -----BEGIN PGP SIGNATURE-----
 Version: GnuPG v1

 iQEcBAABAgAGBQJTbXqDAAoJEMeqX8N+SCQpTBIH/3zCpf0w0+xp8hkwz7dTV9Bw
 ercZp4UpxKV1HgqCxu2r/nGIuZyabLwTis1rcwXOVC4DgRxO0f2BiP0xnyL3OhJu
 CKh8l+HZvvGqVH3Dopm0D/kOxDAWHcjokbyzWBbYJX6WhvT8OI7SSYmwuF4r610h
 hkZ1xgjo4p1x9WegY296PzA1wEe6yy9BvvdIpJHoqBVKClgFrZvtE5PidbrAyLGF
 Kl/2f0K3peBdo6XP0Zaml8NyQlFmAlCV831hHgUmZsBSRpgh/WNvrDSNILTlFJgY
 BOPb2yPP+tiJOXYB66MsjQY9GlX7n43miu5wMtdk1AGqh+26OExbSrZcYVFLk4w=
 =sRee
 -----END PGP SIGNATURE-----

Initial commit

And here are the actual contents of the commit object file:

hexdump .git/objects/87/6793da21833b5b8197b08462523fd6aad3e5ba | \
zlib-decompress | \
bin-to-ascii

commit 671\0tree 70e7c184c3a89c749174b4987830c287fd78952d\nauthor Dan Neumann <danrodneu@gmail.com> 1399683715 -0500\ncommitter Dan Neumann <danrodneu@gmail.com> 1399683715 -0500\ngpgsig -----BEGIN PGP SIGNATURE-----\n Version: GnuPG v1\n \n iQEcBAABAgAGBQJTbXqDAAoJEMeqX8N+SCQpTBIH/3zCpf0w0+xp8hkwz7dTV9Bw\n ercZp4UpxKV1HgqCxu2r/nGIuZyabLwTis1rcwXOVC4DgRxO0f2BiP0xnyL3OhJu\n CKh8l+HZvvGqVH3Dopm0D/kOxDAWHcjokbyzWBbYJX6WhvT8OI7SSYmwuF4r610h\n hkZ1xgjo4p1x9WegY296PzA1wEe6yy9BvvdIpJHoqBVKClgFrZvtE5PidbrAyLGF\n Kl/2f0K3peBdo6XP0Zaml8NyQlFmAlCV831hHgUmZsBSRpgh/WNvrDSNILTlFJgY\n BOPb2yPP+tiJOXYB66MsjQY9GlX7n43miu5wMtdk1AGqh+26OExbSrZcYVFLk4w=\n =sRee\n -----END PGP SIGNATURE-----\n\nInitial commit\n
mkrieger1
  • 19,194
  • 5
  • 54
  • 65
danneu
  • 9,244
  • 3
  • 35
  • 63
  • `git log --show-signature` will verify the signatures for you (see [this question](http://stackoverflow.com/questions/17371955/verifying-signed-git-commits)). So, why - except for just knowing the exact data that's being signed - do you want to mess with git objects on a low level? :) (+1 for the value of your good question. ) – try-catch-finally May 10 '14 at 20:44
  • Great question. I'm exploring the same thing. Basically, the question boils down to this: in order to get `git cat-file -p 876793da21833b5b8197b08462523fd6aad3e5ba | something | gpg --verify -` to yield "Good signature", what is the correct value of `something`? Have you managed to find the answer to that? –  Sep 11 '16 at 16:38

2 Answers2

11

After reading the code in commit_tree_extended, it seems the data used to sign is the part from "tree" to the end of the comment, of course excluding the signature.

In your example, it should be:

tree 70e7c184c3a89c749174b4987830c287fd78952d
author Dan Neumann <danrodneu@gmail.com> 1399683715 -0500
committer Dan Neumann <danrodneu@gmail.com> 1399683715 -0500

Initial commit

From the git source:

Init of buffer:

strbuf_init(&buffer, 8192); /* should avoid reallocs for the headers */
strbuf_addf(&buffer, "tree %s\n", sha1_to_hex(tree));

Parent commits traversing:

/*
* NOTE! This ordering means that the same exact tree merged with a
* different order of parents will be a _different_ changeset even
* if everything else stays the same.
*/
while (parents) {
    struct commit_list *next = parents->next;
    struct commit *parent = parents->item;

    strbuf_addf(&buffer, "parent %s\n",
    sha1_to_hex(parent->object.sha1));
    free(parents);
    parents = next;
}

Person/date information:

if (!author)
    author = git_author_info(IDENT_STRICT);
strbuf_addf(&buffer, "author %s\n", author);
strbuf_addf(&buffer, "committer %s\n", git_committer_info(IDENT_STRICT));
if (!encoding_is_utf8)
    strbuf_addf(&buffer, "encoding %s\n", git_commit_encoding);

while (extra) {
    add_extra_header(&buffer, extra);
    extra = extra->next;
}
strbuf_addch(&buffer, '\n');

The comment & encoding check:

/* And add the comment */
strbuf_addbuf(&buffer, msg);

/* And check the encoding */
if (encoding_is_utf8 && !verify_utf8(&buffer))
    fprintf(stderr, commit_utf8_warn);

That's where the signing happens. Signature will be added after the header.

if (sign_commit && do_sign_commit(&buffer, sign_commit))
    return -1;

There would be parent information too if your commit had some.

ford
  • 10,687
  • 3
  • 47
  • 54
PoKo
  • 126
  • 1
  • 3
  • If it's possible, I would recommend breaking up the code section into smaller parts, so that people don't have to scroll vertically inside to see the whole thing. –  May 12 '14 at 17:38
  • So it's a signature for a SHA-1 digest of the code? Sounds like a bad idea. –  Sep 11 '16 at 21:48
2

This answer is a work in progress.

Background

First, some ruminations on the problems with the current Git signature mechanism.

Ideally, Git would use one of GnuPG's built-in signing mechanisms. If it did so, then it would be easy verify Git commits without having to invoke Git or to write scripts, by simply using GnuPG's gpg --verify or gpg2 --verify.

In this respect, it is a pity that Git did not adopt GnuPG's "detached signature" signing mechanism, as proposed on the Linux Kernel Mailing List in 2005. More recently, Owen Jacobson has listed some additional reasons why detached signatures would be desirable over Git's current approach. He points out that currently:

  • Signatures are embedded within the objects they sign. The signature is part of the object's identity; since Git is content-addressed, this means that an object can neither be retroactively signed nor retroactively stripped of its signature without modifying the object's identity. Git's distributed model means that these sorts of identity changes are both complicated and easily detected.

  • Commit signatures are second-class citizens. They're a relatively recent addition to the Git suite, and both the implementation and the social conventions around them continue to evolve.

  • Only some objects can be signed. While Git has relatively weak rules about workflow, the signature system assumes you're using one of Git's more widespread workflows by limiting your options to at most one signature, and by restricting signatures to tags and commits (leaving out blobs, trees, and refs).

Mike Gerwitz points out one of the most serious ramifications of Git's current approach. Git commits have both a "committer" and an "author" field, allowing for the committer and author of a commit to be two separate people. However, Git commits currently allow the inclusion of only one signature. So, whose signature should it be? Ideally, both the author and the committer would be able to sign the commit. Detached signatures would allow this. So would nested in-line signatures, for that matter. But because Git does not use either of these options, it forces us to choose between two unsatisfactory options:

  1. The committer strips the author's signature and signs a commit themselves.

  2. The committer declines to sign the commit.

That summarises the bad news. The good news is that Git's wrapper for GnuPG does at least include the --show-signature option for git log, which verifies signatures using GnuPG. This allows the Git log to show whether or not a signature was made by a key bearing a UID that you have placed trust in.

If it is, GnuPG will show:

Good signature from "John Doe <john.doe@example.com>"

If not, GnuPG will show:

Good signature from "John Doe <john.doe@example.com>"
WARNING: This key is not certified with a trusted signature!
         There is no indication that the signature belongs to the owner.

Your question

As indicated in Poko's answer, your commit appears, on the face of it, to be equivalent to the following clearsigned document:

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

tree 70e7c184c3a89c749174b4987830c287fd78952d
author Dan Neumann <danrodneu@gmail.com> 1399683715 -0500
committer Dan Neumann <danrodneu@gmail.com> 1399683715 -0500
Initial commit

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1

iQEcBAABAgAGBQJTbXqDAAoJEMeqX8N+SCQpTBIH/3zCpf0w0+xp8hkwz7dTV9Bw
ercZp4UpxKV1HgqCxu2r/nGIuZyabLwTis1rcwXOVC4DgRxO0f2BiP0xnyL3OhJu
CKh8l+HZvvGqVH3Dopm0D/kOxDAWHcjokbyzWBbYJX6WhvT8OI7SSYmwuF4r610h
hkZ1xgjo4p1x9WegY296PzA1wEe6yy9BvvdIpJHoqBVKClgFrZvtE5PidbrAyLGF
Kl/2f0K3peBdo6XP0Zaml8NyQlFmAlCV831hHgUmZsBSRpgh/WNvrDSNILTlFJgY
BOPb2yPP+tiJOXYB66MsjQY9GlX7n43miu5wMtdk1AGqh+26OExbSrZcYVFLk4w=
=sRee
-----END PGP SIGNATURE-----

However, suppose we save that as danneau_stackoverflow_example.asc and attempt to verify it, here's the output:

$ gpg --verify danneau_stackoverflow_example.asc 
gpg: Signature made Sat 10 May 2014 02:01:55 BST
gpg:                using RSA key C7AA5FC37E482429
gpg: Can't check signature: public key not found

What GnuPG means by this is that because I don't have your public key, it can't actually tell whether the signature is good or bad. So, even if I tamper with the contents, I get the same output:

$ echo "-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

tree 70e7c184c3a89c749174b4987830c287fd78952d
author Dan Neumann <danrodneu@gmail.com> 1399683715 -0500
committer Dan Neumann <danrodneu@gmail.com> 1399683715 -0500

EVIL MESSAGE HERE

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1

iQEcBAABAgAGBQJTbXqDAAoJEMeqX8N+SCQpTBIH/3zCpf0w0+xp8hkwz7dTV9Bw
ercZp4UpxKV1HgqCxu2r/nGIuZyabLwTis1rcwXOVC4DgRxO0f2BiP0xnyL3OhJu
CKh8l+HZvvGqVH3Dopm0D/kOxDAWHcjokbyzWBbYJX6WhvT8OI7SSYmwuF4r610h
hkZ1xgjo4p1x9WegY296PzA1wEe6yy9BvvdIpJHoqBVKClgFrZvtE5PidbrAyLGF
Kl/2f0K3peBdo6XP0Zaml8NyQlFmAlCV831hHgUmZsBSRpgh/WNvrDSNILTlFJgY
BOPb2yPP+tiJOXYB66MsjQY9GlX7n43miu5wMtdk1AGqh+26OExbSrZcYVFLk4w=
=sRee
-----END PGP SIGNATURE-----
" | gpg --verify -
gpg: Signature made Sat 10 May 2014 02:01:55 BST
gpg:                using RSA key C7AA5FC37E482429
gpg: Can't check signature: public key not found

So this means that Poko might not have correctly figured out what exactly you signed after all.

In order to get to the bottom of this, I have tried converting commits, signed by private keys whose corresponding public keys I do possess, into clearsigned files, and passing them to GnuPG to verify. So far, I have only ever received "Bad signature" responses. If I figure out where I am going wrong, I will update this answer.

Community
  • 1
  • 1
  • 1
    "Ideally, both the author and the committer would be able to sign the commit." -- if the committer isn't the original author, then the original author isn't the committer and (a) can't and (b) shouldn't sign. – jthill Sep 11 '16 at 22:51
  • Did you find out how to transfer commits into clear signed message? Or were you able to produce the signed content for a detached signature verification? – Dirk Jäckel May 10 '17 at 18:11
  • According to the flags passed to gpg a detached-signature is created – Dirk Jäckel May 10 '17 at 19:25