GitHub ‘Verified’ Commits Can Be Rewritten to New Hashes Without Breaking Signatures

New research shows that Git’s signed hash isn’t the generic name most software thinks it is. Given any signed commit, someone without the signing key can make a second commit with the same files, author, date, and valid signature, GitHub still stamps “Verified.”
All reviewers can check the same. The commit hash does not. That’s important because most systems treat a verified bond’s hash as a permanent, unique name for its content.
Here’s a practical failure: block a malicious link with your hash, and an attacker can re-push the same content under a new, “Verified” hash that has never been seen in your block list. Replication, original logs, and hash-generated reconstruction records gain the same soft spot.
A truncated or hostile mirror can give legitimately signed clones their own hashes that differ from those in the canonical forge.
What this isn’t is a way to slip a different code past the signature check. The files are the same on every copy, so the hash you pinned still fetches exactly the content you expected, even if it fails.
There is no CVE and no vendor advisory, and nothing to change in your repo: the bug is that the fake decides what “Verified” means, and the fix is part of the fake.
The work comes from Jacob Ginesin, a PhD student at Carnegie Mellon University and a cryptographic auditor at Cure53. His five-page paper, posted on arXiv on July 2, comes with a public tool that uses all three attacks, as well as two demo repositories where the exploits still show “Verified” on GitHub.
Because all bindings name their parent with a hash, breaking one action forces new hashes from the actions above it. The tool rewrites that string to keep it consistent. A signed child, however, loses its badge when its parent pointer changes. Ginesin calls the effect “hash chain malleability.”
The cause is signature corruption. A binding’s hash is calculated over everything in it, including the raw signature bytes in its header. Multiple signatures can be rewritten in a different but still valid form, and changing those bytes changes the hash without affecting a line of code.
Three routes include all GPG schemes verified by GitHub, as well as S/MIME:
- ECDSA Keys: solve the signature with a classical piece of elliptic-curve algebra (convert the value of s to n – s). Both forms are valid. This passes the local git verify-commit and earns the GitHub badge.
- RSA and EdDSA keys: add an extra field, which is ignored in the “undamaged” part of the signature, a part the signature doesn’t include on purpose. The signature still checks, but the bytes of the bond, and its hash, change. Local and GitHub both accept it.
- S/MIME (X.509) keys: rewrite the length field in the DER signature structure to a longer, irregular form. A strong local check (via gpgsm) rejects it, but GitHub still marks it as “Verified,” both generated by the tool.
The three routes share one enabler: GitHub does not normalize the signature before checking it out. There is no strong encoding in S/MIME, no stripping of those OpenPGP fields, and non-canonical ECDSA values are accepted as is.
GitHub then places a “verified” record against each commit’s hash and doesn’t recheck it, so the commit remains “Verified” even after its signing key is revoked. Push the original and its twin to two branches, and GitHub’s comparison view treats them as separate histories, one commit forward and one commit backward, without the same files.
To be clear: this is not a hash collision. It does not break SHA-1 or SHA-256, and has nothing to do with Git’s migration to SHA-256. No one forces two separate commits to share the same hash; is a distortion, a single commit can be written in many valid ways, each with its own hash.
The core movement is old. Bitcoin fought a similar ECDSA compromise years ago, where anyone could change the value of s in the transaction signature and change the transaction ID without the owner’s key. The fix was to only accept the “low-S” form, and later extract signatures from the ID via SegWit.
Paper correction is consistent with that: canonicalize the encoding before trusting the hash. Well known course, not new foreign cryptography.
The paper also links this to recent GitHub Actions tag hacks, the 2025 tj-actions/changed-files and the 2026 trivy-action attack (citing the latter). After that, the advice was simple: push the hash of the full commit, not the removable token. That advice still applies.
Pinning stopped that attack, and this study doesn’t change that. Its point is small. In the case of Trivy, the fictitious bonds stood out because they could not be legally signed. This is a warning against relying too much on what is said: a valid signature proves who signed the bond, but does not make the bond’s hash a single word of its contents.
So who should do something? Not a developer who pins an Action or module; the pinned hash still fetches the correct code. The work belongs to the doers. This newspaper says they must register signatures before trusting them.
Tools that block, deduplicate, or record commits must do the same, verifying and canonicaling first rather than relying on a raw hash of a signed object that an attacker can rewrite. Not all systems are presented equally: schemes that repin an independent hash of downloaded files, such as Nix’s static output, save the backstop; those who stand on affirmative devotion do not.
Ginesin says he reported the issue to GNU and Git in January and GitHub in March, and that since the publication of the paper, Git or any hacker has not addressed it. Forge-side fixes are well understood, and the obvious place to start is the S/MIME case, where GitHub still accepts what is rejected by a strong locale check.



