New research shows that a signed Git commit’s hash is not the one-of-a-kind name that much of the software world assumes it to be. Given any signed commit, someone without the signing key can mint a second commit with the same files, author, and date, and a valid signature, GitHub still stamps “Verified.”
Everything a reviewer would check matches. The commit’s hash does not. That matters because so many systems treat a verified commit hash as a permanent, unique name for its contents.
Here is the concrete failure: block a bad commit by its hash, and an attacker can re-push the same content under a fresh, still-“Verified” hash your blocklist has never seen. Deduplication, provenance logs, and reproducible-build records that key on the hash inherit the same soft spot.
A compromised or hostile mirror can hand cloners validly signed commits whose hashes differ from those on the canonical forge.
What this is not is a way to slip different code past a signature check. The files are identical in every copy, so a hash you pinned still fetches exactly the content you expected, or fails.
There is no CVE and no vendor advisory, and nothing to change in your own repo: the flaw is in how a forge decides what “Verified” means, and the fix belongs on the forge’s side.
The work comes from Jacob Ginesin, a PhD student at Carnegie Mellon University and a cryptographic auditor at Cure53. His five-page paper, posted to arXiv on July 2, comes with a public tool that runs all three attacks, plus two demo repositories where the malleated commits still show “Verified” on GitHub.
Because every commit names its parent by hash, malleating one commit forces new hashes on the commits above it. The tool rewrites that chain to keep it consistent. A signed descendant, though, loses its own badge the moment its parent pointer changes. Ginesin calls the effect “hash chain malleability.”
The cause is signature malleability. A commit’s hash is computed over everything inside it, including the raw bytes of the signature in its header. Many signatures can be rewritten into a different but still-valid form, and changing those bytes changes the hash without touching a line of code.
The three routes cover every GPG scheme GitHub verifies, plus S/MIME:
- ECDSA keys: flip the signature with a classic piece of elliptic-curve algebra (turn the value s into n – s). Both forms are valid. This passes a local git verify-commit and earns a GitHub badge.
- RSA and EdDSA keys: add an extra, ignored field to the signature’s “unhashed” section, the part the signature deliberately does not cover. The signature still checks out, but the commit’s bytes, and its hash, change. Local and GitHub both accept it.
- S/MIME (X.509) keys: rewrite a length field in the signature’s DER structure into a longer, non-standard form. A strict local check (via gpgsm) rejects it, but GitHub still marks it “Verified,” both of which the tool reproduces.
The three routes share one enabler: GitHub does not normalize a signature before checking it. No strict encoding on S/MIME, no stripping of those OpenPGP fields, and non-canonical ECDSA values accepted as-is.
GitHub then files a “Verified” record against each commit hash and does not re-check it, so a commit stays “Verified” even after its signing key is revoked. Push an original and its twin to two branches, and GitHub’s compare view treats them as divergent histories, one commit ahead and one behind, despite identical 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 move to SHA-256. Nobody is forcing two different commits to share one hash; it is the reverse, one commit that can be written many valid ways, each with its own hash.
The core move is old. Bitcoin fought the exact same ECDSA symmetry years ago, when anyone could flip the s value in a transaction signature and change the transaction’s ID without the owner’s key. The fix was to accept only the “low-S” form, and later to move signatures out of the ID with SegWit.
The paper’s fixes rhyme with that: canonicalize the encoding before you trust the hash. A known lesson, not exotic new cryptography.
The paper also connects this to the recent GitHub Actions tag hijacks, the 2025 tj-actions/changed-files and 2026 trivy-action attacks (it cites the latter). After those, the advice was simple: pin to a full commit hash, not a movable tag. That advice still holds.
Pinning stopped those attacks, and this research does not change that. Its point is narrower. In the Trivy case, the malicious commits stood out because they could not be validly signed. This is a caution against leaning too hard on that tell: a valid signature proves who signed a commit, but it does not make the commit’s hash a one-of-a-kind name for what it contains.
So who has to do something? Not the developer pinning an Action or a module; a pinned hash still fetches the right code. The work is for the forges. The paper says they should canonicalize signatures before trusting them.
Tooling that blocks, deduplicates, or records provenance by commit hash should do the same, verifying and canonicalizing first rather than trusting the raw hash of a signed object that an attacker can re-encode. Not all systems are equally exposed: schemes that also pin an independent hash of the fetched files, such as Nix’s fixed-output derivations, keep a backstop; those that stop at a verified commit hash do not.
Ginesin says he reported the issue to GNU and Git in January and to GitHub in March, and that as of the paper’s publication, neither Git nor any forge had addressed it. The forge-side fix is well understood, and the obvious place to begin is the S/MIME case, where GitHub still accepts what a strict local check rejects.
Found this article interesting? Follow us on Google News, Twitter and LinkedIn to read more exclusive content we post.

