A verified Git commit can now be treated as two different infrastructure objects while carrying the same tree, the same metadata, and a valid signature. That is the punchline from Jacob Ginesin’s July 7 preprint, Git Hash Chain Malleability, and it cuts through a decade of sloppy supply-chain shorthand around signed commits.
This is a revisit of Git 3.0 Turns Version Control Into Migration Plumbing and the more recent Guix substitute trust pipeline. The new development is specific: Ginesin shows three ways to alter the bytes of a signed Git commit’s embedded signature so the commit gets a different hash while verification still succeeds. SHA-256 does not fix this. Reproducible-build discipline does not magically fix it either. The weak joint is byte canonicalization around the signature container, then every downstream system that treats the resulting commit hash as a unique policy key.
The mechanism is ugly in the best way: small, real, and sitting exactly where infrastructure folklore gets lazy. A Git commit object includes the tree, parents, author and committer lines, message, and, for signed commits, a gpgsig header. Git signs the commit payload with the signature header removed. Then the signature bytes get embedded back into the object, and the whole serialized object receives the commit hash.
That layout creates the gap. Change the signature bytes without changing the signed payload, and the tree, metadata, message, and signature validity can all survive. The commit hash still changes because the hash covers those changed signature bytes.
Ginesin names three routes. For ECDSA, the old signature-malleability trick maps (r, s) to (r, n-s), producing another valid signature over the same message. For RSA and EdDSA inside OpenPGP, the proof of concept appends ignored advisory data into an unhashed subpacket region described by RFC 4880. For S/MIME/CMS, it rewrites an ASN.1 length field into a non-canonical encoding that a permissive BER parser accepts even though strict DER rejects it. The paper says ECDSA, RSA, and EdDSA pass local git verify-commit; S/MIME fails locally through gpgsm but still receives GitHub verification.
The Github part matters because the server-side record is durable and keyed on the commit hash. GitHub’s documentation on commit signature verification says a verified commit keeps its verification record even if the signing key later expires, is revoked, or disappears from the account. That persistence is useful when the record means what people think it means. Here it becomes a badge duplicator. The paper reports that GitHub accepts all three malleation families and creates independent Verified records for the altered commits.
So the problem is not that Git’s hash function has been cracked. Nobody is presenting two different byte strings with the same SHA. The problem is stranger: one logical signed commit can have multiple byte-level encodings, each with its own hash. If your control plane keys on raw commit hashes while assuming signature verification makes those hashes unique content identities, your control plane has a hole shaped like a parser.
This lands right on the earlier Git 3.0 story. Git’s own hash-function-transition documentation explains the SHA-256 migration as a path away from SHA-1 weakness. It says cryptographic object names let third parties trust that a hash addresses a signed object and all objects it references. That remains true for the bytes. It does not imply that every verified signature container has one canonical byte representation. SHA-256 gives you stronger names for objects. It does not decide which semantically equivalent signed object encodings should be legal.
LWN’s February coverage of Patrick Steinhardt’s FOSDEM talk on Git’s next decade framed the same ecosystem pressure from the other side: Git can ship SHA-256 support, reftables, large-object promisors, and cleaner history editing, but forges and tools still determine when those mechanics become real infrastructure. This paper is another version of that lesson. Git’s object model can be precise while the surrounding forge, verification, package, and provenance systems quietly smuggle in looser assumptions.
The dependency-pinning angle is where this gets less academic. GitHub Actions culture has spent years repeating “pin by full commit SHA” as the serious person’s answer to mutable tags. That advice still beats floating tags. It also becomes weaker than advertised when a signed commit can keep its contents and grow a sibling hash. Nix flakes, Go pseudo-versions, Dockerfiles that fetch source at a commit, SLSA source provenance, Sigstore Gitsign records, and internal deployment allowlists all have variants of the same habit: convert a Git hash into an authority token.
Some systems get a second line of defense. Nix fixed-output derivations can pair the revision with an independent content hash of the fetched tree. A build system that verifies the tree digest, canonical source archive hash, or recorded provenance bundle has more to work with than the commit hash alone. A tool that calls git verify-commit, sees success, and treats the SHA as a unique identity has less.
The nasty bit is hash-based blocking. Incident response often moves faster than architecture. A malicious commit appears. A service blocks that SHA. A platform reverts that SHA. A workflow denies that SHA. If the malicious actor can mint an equivalent signed commit with a different hash, the blocklist becomes a speed bump with a Verified sticker on it. The content does not need to change. The signature key does not need to leak. The hash-based control misses because it was guarding the name, not the canonical object.
The proof-of-concept repo, git-chain-malleator, is small enough to make the issue uncomfortable. It wraps the GPG routes in interactive-malleator.py, uses setup-ghost.py for S/MIME, ships per-algorithm tests, and presents the expected matrix: ECDSA, RSA, and EdDSA pass local verification and GitHub; S/MIME fails local verification but passes GitHub. The README is not a product page. It is a lab bench with a bad smell coming from the verification stack.
The fix shape is boring because real fixes usually are. Enforce low-s ECDSA. Strip or reject OpenPGP unhashed subpacket material before deriving identity from a signature. Enforce DER canonicalization for CMS instead of accepting BER convenience. Tie Verified records to canonicalized signature forms and signed payload digests, not only raw commit hashes. Teach dependency managers and policy engines to compare source trees or source archive hashes when they care about content equivalence. Keep commit SHAs as addresses, not as theology.
The old comfort was simple: signed commit plus full SHA plus Verified badge equals stable provenance. That comfort was always too neat. Git’s object graph is a machine made of bytes, and signatures are containers with their own parser histories, leniencies, and weird little compatibility pits.
The better rule is harsher and more useful: verified hashes are addresses produced after parsing, not canonical proofs of unique content. If a supply-chain system wants uniqueness, it has to earn it with canonicalization, tree comparison, source hashes, and provenance checks that survive a signature byte getting clever. Anything else is badge worship with extra steps.