Forging signed commits on GitHub
A half-year in the past, I discovered a bug in an inner GitHub API that allow me trick the interior API into signing commits as any consumer. So I may create a commit signed by a consumer I don’t management:
Earlier than I clarify how I did that, first some context on how Git commit signing works:
Git commit internals
Git commits are saved in a customized text-based format that appears like:
tree 55ca6286e3e4f4fba5d0448333fa99fc5a404a73
mother or father 7676f1f3b526f05b530a3566211dab5a5225af9a
writer loops <me@iter.ca> 1678388328 -0500
committer loops <me@iter.ca> 1678388328 -0500
Commit message
Signed commits have a further gpgsig
header that has a signature over each line within the commit besides the gpgsig
itself:
tree be0788944df13c5d170e050f2fe178360c3df5a5
writer loops <me@iter.ca> 1678388328 -0500
committer loops <me@iter.ca> 1678388328 -0500
gpgsig -----BEGIN PGP SIGNATURE-----
iQIzBAABCgAdFiEEK6cyil0jdmW2bZPmDXSPTJUzsugFAmQKLGgACgkQDXSPTJUz
[...]
=756m
-----END PGP SIGNATURE-----
Official signed commit
Once you create a commit on GitHub.com, it’s signed with GitHub’s web flow GPG key and has a committer
of GitHub <noreply@github.com>
. Internet-flow signed commits are proven as signed within the GitHub UI:
If we will trick GitHub into signing a commit with any writer
, we will create solid commits that GitHub reveals as signed.
Tricking an inner API endpoint into signing our commits
For some context, I realized how some GitHub internals work by downloading a GitHub Enterprise Server trial VM and deobfuscating the Ruby supply code on the VM.
GitHub Codespaces is a GitHub service that gives you with a growth setting within the cloud. One characteristic of Codespaces is that commits created in it are signed with the net move GPG key (if enabled in settings).
When a commit is created in a codespace with GPG signing enabled, this occurs:
- git checks the
gpg.program
config possibility, which is ready to/.codespaces/bin/gh-gpgsign
- git calls
/.codespaces/bin/gh-gpgsign
with the uncooked commit physique - The
gh-gpgsign
binary (which is closed-source) makes an API request tohttps://api.github.com/vscs_internal/commit/signal
with the commit physique gh-gpgsign
returns the signature returned from the API- git inserts the commit signature into the commit information
The /vscs_internal/commit/signal
endpoint is attention-grabbing since you can provide it arbitrary uncooked commit information, and get a signature again.
That /vscs_internal/commit/signal
endpoint checked that the writer line within the supplied commit information is legitimate by discovering the primary line that matches the regex /Aauthor (.+?) <(.+)>/
, and guaranteeing the identify and e-mail extracted from that regex corresponded to the logged-in consumer. However this regex doesn’t match writer strains with 0-length names! So for this commit:
tree 251966888982546b81f8bfc8de1f25077f099a56
mother or father fb5ce469856769a17cca88ec4e2c6159d4669b21
writer <583231+octocat@customers.noreply.github.com> 1682188800 +0000
committer GitHub <noreply@github.com> 1682188800 +0000
writer username <consumer@instance.com> 1682188800 +0000
commit message
For the reason that first writer
identify is zero characters lengthy, the regex skips that line, and the pretend second writer line is used as an alternative. Git ignores further writer
strains after the primary, so Codespaces appears to be like on the second writer line however Git appears to be like on the first. This implies we will create GitHub-signed commits with any writer identify+e-mail.
The repair
GitHub fastened the problem by altering the problematic regex to /Aauthor ([^<]*)[ ]{0,1}<(.+)>/
, which ought to all writer
header strains accepted by git-fsck
.
Timeline
- April 22 2023: I report the problem to GitHub
- April 24 2023: GitHub closes the problem, saying that having the ability to impersonate your individual account will not be a difficulty
- April 24 2023: I reply saying that you should utilize this to assault impersonate different individuals
- Might 2 2023: I reply once more, demonstrating that the problem nonetheless works
- Might 4 2023: GitHub reopens the problem
- Might 17 2023: GitHub validates the problem and begins work on a repair
- June 2023: GitHub fixes the problem on GitHub.com
- June 23 2023: GitHub closes the problem and rewards me with $10000