This post is about PGP signature verification of git commits.
- Verify by taking things apart
- Verify using git cli
- Verify using golang’s crypto/openpgp package
- Verify using src-d/go-git
Git commits and any kind of data can be signed using GPG (GNU Privacy Guard) program, available at https://www.gnupg.org/. It implements the OpenPGP standard for encryption.
4 Things
Data
This is the data/content that is to be communicated or published and requires signing for verifiable authenticity. This can be public.
Private Key
This is a secret key which is used to sign data/content. It has an associated public key. This must never be public.
Public Key
This is a publicly available key which can be used to verify a signed document. It has an associated private key. This should be openly distributed and should be public.
Signature
This could be a binary blob or ASCII text, generated as a separate file when detached, or part of document that’s signed. This can be public.
The above 4 things are related as:
Data + Private Key => Signed Document
Data + Signature + Public Key => Signature Verification
The data document can be signed using a private key to generate a signed document.
The signed document can be verified that it has not be tampered and is created by the person who says it’s from.
These 4 things are involved in PGP signature verification.
git commit signing
In case of git commits, the commit objects are stored in the filesystem under
.git/objects/
, which would have subdirectories with commit hash prefix.
$ ls .git/objects/
00/ 09/ 16/ 1d/ 27/ 32/ 3c/ 46/ 53/ 5c/ 69/ 77/
91/ 9b/ a1/ ac/ b6/ bd/ c6/ d0/ dc/ e6/ f2/ f9/
01/ 0b/ 17/ 1e/ 29/ 34/ 3e/ 47/ 54/ 5d/ 6a/ 79/
92/ 9c/ a2/ ae/ b7/ be/ c8/ d3/ dd/ e7/ f3/ fb/
Inside these directories are the compressed binary commit files. They can be
read using git cat-file
.
$ git cat-file -p 8a9cea36fe052711fbc42b86e1f99a4fa0065deb
tree 6572ba6df4f1fb323c8aaa24ce07bca0648b161e
parent ede5f57ea1280a0065beec96d3e1a3453d010dbd
author darkowlzz <example@darkowlzz.space> 1511197315 +0000
committer darkowlzz <example@darkowlzz.space> 1511197315 +0000
gpgsig -----BEGIN PGP SIGNATURE-----
iQFHBAABCAAxFiEEoRt6IzxHaZkkUslhQyLeMqcmyU4FAloTCrsTHG1lQGRhcmtv
d2x6ei5zcGFjZQAKCRBDIt4ypybJTul5CADmVxB4kqlqRZ9fAcSU5LKva3GRXx0+
leX6vbzoyQztSWYgl7zALh4kB3a3t2C9EnnM6uehlgaORNigyMArCSY1ivWVviCT
BvldSVi8f8OvnqwbWX0I/5a8KmItthDf5WqZRFjhcRlY1AK5Bo2hUGVRq71euf8F
rE6wNhDoyBCEpftXuXbq8duD7D6qJ7QiOS4m5+ej1UCssS2WQ60yta7q57odduHY
+txqTKI8MQUpBgoTqh+V4lOkwQQxLiz7hIQ/ZYLUcnp6fan7/kY/G7YoLt9pOG1Y
vLzAWdidLH2P+EUOqlNMuVScHYWD1FZB0/L5LJ8no5pTowQd2Z+Nggxl
=0uC8
-----END PGP SIGNATURE-----
some commit message
-p
flag is for pretty-printing the content. And that’s what a commit file
contains. tree
, parent
, author
, committer
, message
and signature
.
Some of them might be empty depending on the git tree and the type of commit.
An unsigned commit would not have the gpgsig
signature but other parts would
be the same.
Verify by taking things apart
The document content of this commit that is signed when a commit is signed is:
tree 6572ba6df4f1fb323c8aaa24ce07bca0648b161e
parent ede5f57ea1280a0065beec96d3e1a3453d010dbd
author darkowlzz <example@darkowlzz.space> 1511197315 +0000
committer darkowlzz <example@darkowlzz.space> 1511197315 +0000
some commit message
Say, we put this data in a file commit.txt
.
The signature is:
-----BEGIN PGP SIGNATURE-----
iQFHBAABCAAxFiEEoRt6IzxHaZkkUslhQyLeMqcmyU4FAloTCrsTHG1lQGRhcmtv
d2x6ei5zcGFjZQAKCRBDIt4ypybJTul5CADmVxB4kqlqRZ9fAcSU5LKva3GRXx0+
leX6vbzoyQztSWYgl7zALh4kB3a3t2C9EnnM6uehlgaORNigyMArCSY1ivWVviCT
BvldSVi8f8OvnqwbWX0I/5a8KmItthDf5WqZRFjhcRlY1AK5Bo2hUGVRq71euf8F
rE6wNhDoyBCEpftXuXbq8duD7D6qJ7QiOS4m5+ej1UCssS2WQ60yta7q57odduHY
+txqTKI8MQUpBgoTqh+V4lOkwQQxLiz7hIQ/ZYLUcnp6fan7/kY/G7YoLt9pOG1Y
vLzAWdidLH2P+EUOqlNMuVScHYWD1FZB0/L5LJ8no5pTowQd2Z+Nggxl
=0uC8
-----END PGP SIGNATURE-----
The above signature is an example of ASCII-armored signature. The space at the beginning of the lines are wrapped by most of the pgp implementations, so that shouldn’t be an issue.
Say, we put this signature is another file doc.sig
.
This way of separating the data and signature is referred to as Detached
Signature
.
To verify a document with a given signature, we also need a public key of the
signer. A program like gpg uses a keyring
to store known public keys. It’s
the user’s responsibility to verify and keep the public keys up-to-date.
Using gpg, we can verify commit.txt
with signature doc.sig
by running:
$ gpg --verify doc.sig commit.txt
If the signer is in the gpg keyring, that would be returned as the output. Else, the verification would fail.
NOTE: Even a slight change in the content of commit.txt
(a whitespace,
newline, etc) would result in a failed verification.
CATCH: The commit message at the bottom of the commit file ends with a
newline, which isn’t visible. Absence of this newline also results in vailed
verification. An example would be in case of verification with src-d/go-git
.
Verify using git cli
git
cli tool has a subcommand verify-commit
to perform the signature
verification.
$ git verify-commit 8a9cea36fe052711fbc42b86e1f99a4fa0065deb
The way this works internally, as per the git/gpg-interface.c
,
is by exporting the signature into a temporary file and passing the signature
file to gpg
tool with the commit content file. The gpg tool refers to the
default keyring
for signer’s public key. This is the same as manually
verifying using gpg tool.
Verify using golang’s crypto/openpgp package
golang.org/x/crypto/openpgp
has a function CheckArmoredDetachedSignature()
to perform signature verification. Since this is not relying on gpg
, it
requires the keyring to be passed as an argument.
func CheckArmoredDetachedSignature(keyring KeyRing, signed, signature io.Reader) (signer *Entity, err error)
Since we have been using armored signature, we can use an armored keyring as well. Export an armored keyring with public key:
$ gpg --armor --export you@example.com > mykey.asc
This file would contain public key of you@example.com user only, from the
default keyring. Now, this can be read and an io.Reader
can be created and
passed to CheckArmoredDetachedSignature()
as the keyring. Or even the public
key string can be used directly to create a reader as:
armoredKeyRingReader := strings.NewReader(`
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFmtHgABCADnfThM7q8D4pgUub9jMppSpgFh3ev84g3Csc3yQUlszEOVgXmu
YiSWP1oAiWFQ8ahCydh3LT8TnEB2QvoRNiExUI5XlXFwVfKW3cpDu8gdhtufs90Q
......
-----END PGP PUBLIC KEY BLOCK-----
`)
signature := strings.NewReader(`
-----BEGIN PGP SIGNATURE-----
iQFHBAABCAAxFiEEoRt6IzxHaZkkUslhQyLeMqcmyU4FAloTCrsTHG1lQGRhcmtv
d2x6ei5zcGFjZQAKCRBDIt4ypybJTul5CADmVxB4kqlqRZ9fAcSU5LKva3GRXx0+
leX6vbzoyQztSWYgl7zALh4kB3a3t2C9EnnM6uehlgaORNigyMArCSY1ivWVviCT
BvldSVi8f8OvnqwbWX0I/5a8KmItthDf5WqZRFjhcRlY1AK5Bo2hUGVRq71euf8F
rE6wNhDoyBCEpftXuXbq8duD7D6qJ7QiOS4m5+ej1UCssS2WQ60yta7q57odduHY
+txqTKI8MQUpBgoTqh+V4lOkwQQxLiz7hIQ/ZYLUcnp6fan7/kY/G7YoLt9pOG1Y
vLzAWdidLH2P+EUOqlNMuVScHYWD1FZB0/L5LJ8no5pTowQd2Z+Nggxl
=0uC8
-----END PGP SIGNATURE-----
`)
commitData, err := os.Open("commit.txt")
...
entity, err := openpgp.CheckArmoredDetachedSignature(armoredKeyRingReader, commitData, signature)
In the above code, readers are created using strings.NewReader
and os.Open
.
All the inline keys can be read from file using os.Open
and their readers
could be used in the same way as above to perform a verification.
Verify using go-git
gopkg.in/src-d/go-git.v4/plumbing/object
’s Commit type has Verify
method to
perform verification of a commit, given an armored keyring.
func (c *Commit) Verify(armoredKeyRing string) (*openpgp.Entity, error)
This is same as golang.org/x/crypto/openpgp
but the commit information is
implicit. Only an armored keyring is passed.
An example commit object can be created and verified as:
ts := time.Unix(0000000000, 0)
commit := &Commit{
Hash: plumbing.NewHash("8a9cea36fe052711fbc42b86e1f99a4fa0065deb"),
Author: Signature{Name: "darkowlzz", Email: "example@darkowlzz.space", When: ts},
Committer: Signature{Name: "darkowlzz", Email: "example@darkowlzz.space", When: ts},
Message: `status: simplify template command selection
`,
TreeHash: plumbing.NewHash("6572ba6df4f1fb323c8aaa24ce07bca0648b161e"),
ParentHashes: []plumbing.Hash{plumbing.NewHash("ede5f57ea1280a0065beec96d3e1a3453d010dbd")},
PGPSignature: `
-----BEGIN PGP SIGNATURE-----
iQFHBAABCAAxFiEEoRt6IzxHaZkkUslhQyLeMqcmyU4FAloTCrsTHG1lQGRhcmtv
d2x6ei5zcGFjZQAKCRBDIt4ypybJTul5CADmVxB4kqlqRZ9fAcSU5LKva3GRXx0+
leX6vbzoyQztSWYgl7zALh4kB3a3t2C9EnnM6uehlgaORNigyMArCSY1ivWVviCT
BvldSVi8f8OvnqwbWX0I/5a8KmItthDf5WqZRFjhcRlY1AK5Bo2hUGVRq71euf8F
rE6wNhDoyBCEpftXuXbq8duD7D6qJ7QiOS4m5+ej1UCssS2WQ60yta7q57odduHY
+txqTKI8MQUpBgoTqh+V4lOkwQQxLiz7hIQ/ZYLUcnp6fan7/kY/G7YoLt9pOG1Y
vLzAWdidLH2P+EUOqlNMuVScHYWD1FZB0/L5LJ8no5pTowQd2Z+Nggxl
=0uC8
-----END PGP SIGNATURE-----
`,
}
entity, err := commit.Verify(armoredKeyRing)
...
NOTE: The commit message contains a newline. While implementing this verification feature in go-git, I spent a long time debugging what was causing verification to fail, only to find out that a newline was missing from the commit message. And that’s one of the reasons for writing this post 😅