openSUSE:ALP/Workgroups/Git-Packaging-Workflow/WorkPackages/Multi-signature review handling idea
Prerequisites: Read the Commits are snapshots, not diffs and Multiple signatures in a git commit
The central problem that we are trying to solve has the following requirements
- cryptographically secure and immutable reviews
- out of the way -- not visible to regular git users
- part of the git repository and its history -- no additional infrastructure
The solution is to use a GPG signature as a means of approving a git commit. The caveat is we will need multiple reviews which requires multiple GPG signatures and this document describes how to do this.
What is a signed commit?
Anatomy of a commit message
First, we need to look at what is a commit message. Let's look at what is a typical commit
> git cat-file -t ab3743373248a8758f07903e2b7eb9239d53bf73 commit > git cat-file -p ab3743373248a8758f07903e2b7eb9239d53bf73 tree f05af273ba36fe5176e5eaab349661a56b3d27a0 author Adam Majer <amajer@suse.de> 1663841134 +0200 committer Adam Majer <amajer@suse.de> 1663841134 +0200 Initial unsigned commit
So, the commit hash id is ab3743373248a8758f07903e2b7eb9239d53bf73, this is not actually part of the commit message. The commit message looks like a mail message, consisting of headers like tree, author, committer, and the actual text of the commit message that follows. This is the text that is actually signed and thus it is impossible to modify this message after it's signed. This means, no "signed-off" tags can be added after it's signed.
The signature
Let's look and manually verify an actual signed commit with
git commit -S
> git cat-file -p 2b549e034613c67f32fb3875dd61cdd7bd585052 tree 1ddaed3346e45f5f50468c1118a88b1e65547b4f parent ab3743373248a8758f07903e2b7eb9239d53bf73 author Adam Majer <amajer@suse.de> 1663841363 +0200 committer Adam Majer <amajer@suse.de> 1663841427 +0200 gpgsig -----BEGIN PGP SIGNATURE----- iHUEABYKAB0WIQTtsx4h0JQo2B9B5shla2XtpJuLkAUCYyw0lAAKCRBla2XtpJuL kDiuAP9H9OJt5FuGIggzVKtTdYI2BtlJxOAcev98RtVVJ3AWywEAxIZ4GurUVyTV s/ozaDjC707BSHcBbL3mzsMhTvgbOQ4= =bfsh -----END PGP SIGNATURE----- Adding a new line, because the best!
So, now we can separate the two parts of this, the gpgsig and the rest. Notice that gpgsig is a multi-line header and its value is an armored signature indented with single space. So, splitting the two yields,
tree 1ddaed3346e45f5f50468c1118a88b1e65547b4f parent ab3743373248a8758f07903e2b7eb9239d53bf73 author Adam Majer <amajer@suse.de> 1663841363 +0200 committer Adam Majer <amajer@suse.de> 1663841427 +0200 Adding a new line, because the best!
and a detached signature
-----BEGIN PGP SIGNATURE----- iHUEABYKAB0WIQTtsx4h0JQo2B9B5shla2XtpJuLkAUCYyw0lAAKCRBla2XtpJuL kDiuAP9H9OJt5FuGIggzVKtTdYI2BtlJxOAcev98RtVVJ3AWywEAxIZ4GurUVyTV s/ozaDjC707BSHcBbL3mzsMhTvgbOQ4= =bfsh -----END PGP SIGNATURE-----
which can now be verified,
gpg --verify data.sig data gpg: Signature made Thu 22 Sep 2022 12:10:28 PM CEST gpg: using EDDSA key EDB31E21D09428D81F41E6C8656B65EDA49B8B90 gpg: Good signature from "test2" [ultimate]
Appending additional signatures
Signing
Since the signature is detached, we can actually combine multiple signatures into one. Here we sign with test1 key while the previous commit is signed with test2 key.
gpg --dearmor data.sig gpg -u test1 -o data.sig2.gpg --detach-sign data cat data.sig.gpg data.sig2.gpg | gpg --enarmor > data.newsig
And now the newsig is,
-----BEGIN PGP ARMORED FILE----- Comment: Use "gpg --dearmor" for unpacking iHUEABYKAB0WIQTtsx4h0JQo2B9B5shla2XtpJuLkAUCYyw0lAAKCRBla2XtpJuL kDiuAP9H9OJt5FuGIggzVKtTdYI2BtlJxOAcev98RtVVJ3AWywEAxIZ4GurUVyTV s/ozaDjC707BSHcBbL3mzsMhTvgbOQ6IdQQAFgoAHRYhBEUByzN6sbYKKVuun+E1 Qd3Y6dOVBQJjLHbtAAoJEOE1Qd3Y6dOVSCMA/1ynEhW6L8yB3eZpyfApO98jor3G +B7duoI1+HRy6er0AQDJKogPbsRMWmwJfMF3J7j1aesCTYU8tPjmTNlTI7g/BQ== =dVSf -----END PGP ARMORED FILE-----
which contains both signatures
> gpg --verify data.newsig data gpg: Signature made Thu 22 Sep 2022 12:10:28 PM CEST gpg: using EDDSA key EDB31E21D09428D81F41E6C8656B65EDA49B8B90 gpg: Good signature from "test2" [ultimate] gpg: Signature made Thu 22 Sep 2022 04:53:33 PM CEST gpg: using EDDSA key 4501CB337AB1B60A295BAE9FE13541DDD8E9D395 gpg: Good signature from "test1" [ultimate]
Now we can re-insert this back onto the commit message
> cat data.new tree 1ddaed3346e45f5f50468c1118a88b1e65547b4f parent ab3743373248a8758f07903e2b7eb9239d53bf73 author Adam Majer <amajer@suse.de> 1663841363 +0200 committer Adam Majer <amajer@suse.de> 1663841427 +0200 gpgsig -----BEGIN PGP ARMORED FILE----- Comment: Use "gpg --dearmor" for unpacking iHUEABYKAB0WIQTtsx4h0JQo2B9B5shla2XtpJuLkAUCYyw0lAAKCRBla2XtpJuL kDiuAP9H9OJt5FuGIggzVKtTdYI2BtlJxOAcev98RtVVJ3AWywEAxIZ4GurUVyTV s/ozaDjC707BSHcBbL3mzsMhTvgbOQ6IdQQAFgoAHRYhBEUByzN6sbYKKVuun+E1 Qd3Y6dOVBQJjLDdpAAoJEOE1Qd3Y6dOVco4A/3NTnsqPJuFFKtEEEuyGXTZrpuGq RgJEBfLzNUMDAvndAQCE8RyGtrHnZnBMp9/SULS8JC/TYoJgED3xsKyZ6ztZCA== =SCfn -----END PGP ARMORED FILE----- Adding a new line, because the best!
And insert it into the git tree
git hash-object -t commit -w data.new bd5956bc92721000b19dca778c4dbb9d884d39e0 > git cat-file -p bd5956bc92721000b19dca778c4dbb9d884d39e0 tree 1ddaed3346e45f5f50468c1118a88b1e65547b4f parent ab3743373248a8758f07903e2b7eb9239d53bf73 author Adam Majer <amajer@suse.de> 1663841363 +0200 committer Adam Majer <amajer@suse.de> 1663841427 +0200 gpgsig -----BEGIN PGP ARMORED FILE----- Comment: Use "gpg --dearmor" for unpacking iHUEABYKAB0WIQTtsx4h0JQo2B9B5shla2XtpJuLkAUCYyw0lAAKCRBla2XtpJuL kDiuAP9H9OJt5FuGIggzVKtTdYI2BtlJxOAcev98RtVVJ3AWywEAxIZ4GurUVyTV s/ozaDjC707BSHcBbL3mzsMhTvgbOQ6IdQQAFgoAHRYhBEUByzN6sbYKKVuun+E1 Qd3Y6dOVBQJjLDdpAAoJEOE1Qd3Y6dOVco4A/3NTnsqPJuFFKtEEEuyGXTZrpuGq RgJEBfLzNUMDAvndAQCE8RyGtrHnZnBMp9/SULS8JC/TYoJgED3xsKyZ6ztZCA== =SCfn -----END PGP ARMORED FILE----- Adding a new line, because the best!
NOTE: Every time you add a signature, you change the commit message. This will this change the commit id which will require any branch to be reset although the original signature will remain valid.
Verification
Verification via git-verify-commit fails since it explicitly now refuses multi-signed messages. But manual verification is quite simple. For example, assuming we have multiple keyrings. One keyring for reviewers and one keyring for bots. Then we can verify whether signatures are completed with gpg, this time let's use status messages of gpgv2
1. extract the signature from the commit message 2. run gpgv2 with correct keyring to check if it was signed by the appropriate reviewer 3. when all reviewers have signed, the commit is verified.
So using data in above,
> gpgv2 --keyring ./keyring.bot data.newsig data gpgv: Signature made Thu 22 Sep 2022 12:10:28 PM CEST gpgv: using EDDSA key EDB31E21D09428D81F41E6C8656B65EDA49B8B90 gpgv: Good signature from "test2" gpgv: Signature made Thu 22 Sep 2022 04:53:33 PM CEST gpgv: using EDDSA key 4501CB337AB1B60A295BAE9FE13541DDD8E9D395 gpgv: Can't check signature: No public key > gpgv2 --keyring ./keyring.audit data.newsig data gpgv: Signature made Thu 22 Sep 2022 12:10:28 PM CEST gpgv: using EDDSA key EDB31E21D09428D81F41E6C8656B65EDA49B8B90 gpgv: Can't check signature: No public key gpgv: Signature made Thu 22 Sep 2022 04:53:33 PM CEST gpgv: using EDDSA key 4501CB337AB1B60A295BAE9FE13541DDD8E9D395 gpgv: Good signature from "test1"
In machine-readable format,
> gpgv2 --keyring ./keyring.bot --status-fd 1 data.newsig data 2> /dev/null [GNUPG:] NEWSIG [GNUPG:] KEY_CONSIDERED EDB31E21D09428D81F41E6C8656B65EDA49B8B90 0 [GNUPG:] SIG_ID TsXgTrBHSsIjQkHE5b8S5isAdtw 2022-09-22 1663841428 [GNUPG:] KEY_CONSIDERED EDB31E21D09428D81F41E6C8656B65EDA49B8B90 0 [GNUPG:] GOODSIG 656B65EDA49B8B90 test2 [GNUPG:] VALIDSIG EDB31E21D09428D81F41E6C8656B65EDA49B8B90 2022-09-22 1663841428 0 4 0 22 10 00 EDB31E21D09428D81F41E6C8656B65EDA49B8B90 [GNUPG:] NEWSIG [GNUPG:] ERRSIG E13541DDD8E9D395 22 10 00 1663858413 9 4501CB337AB1B60A295BAE9FE13541DDD8E9D395 [GNUPG:] NO_PUBKEY E13541DDD8E9D395 > gpgv2 --keyring ./keyring.audit --status-fd 1 data.newsig data 2> /dev/null [GNUPG:] NEWSIG [GNUPG:] ERRSIG 656B65EDA49B8B90 22 10 00 1663841428 9 EDB31E21D09428D81F41E6C8656B65EDA49B8B90 [GNUPG:] NO_PUBKEY 656B65EDA49B8B90 [GNUPG:] NEWSIG [GNUPG:] KEY_CONSIDERED 4501CB337AB1B60A295BAE9FE13541DDD8E9D395 0 [GNUPG:] SIG_ID 2VZOyE4QY+46/m3HJ+FhuZgsiAk 2022-09-22 1663858413 [GNUPG:] KEY_CONSIDERED 4501CB337AB1B60A295BAE9FE13541DDD8E9D395 0 [GNUPG:] GOODSIG E13541DDD8E9D395 test1 [GNUPG:] VALIDSIG 4501CB337AB1B60A295BAE9FE13541DDD8E9D395 2022-09-22 1663858413 0 4 0 22 10 00 4501CB337AB1B60A295BAE9FE13541DDD8E9D395
As long as we have signatures of all the required entities, we can merge this request. More importantly, it's not possible to add additional signatures once the commit is merged onto the target.
Advantages
- Immutable once submitted to review since signatures will fail if the tree object is modified
- No additional commits to pollute the 'git log'
- As long as no conflicts, it can be "rebased" in a merge commit
- You can submit the same signed commit to multiple branches and reviews are then added independently (by definition)
Challenges
Tooling
Multiply signed commits are not supported by 'git verify-commit' because they are simply not used. This is small tooling issue. The main counter-argument here is that reviews are done either with UI or signatures can be merged on the backend. We do not need to rely on external tool support here. This makes this less of a challenge.
Even simple reviews where signature is replaced via a resigned, amended commit, can be simply merged on the backend,
gpg --amend -S git push gitea new_main:PR1233 ...here we can merge sigs in a server-side commit hook...
Keyring management
Additionally, 'git verify-commit' does not support specifying keyrings which makes it less useful than doing this verification manually. At very least, we will have to manage the keyrings containing the keys of the bots and reviewers. The keys of submitters are less important, just like in our current reviews. See hhttps://gitlab.com/source-security/git-verify/ for a tool that set out to support managing keyrings.