Your security scanning tool flags a malicious commit. You block it by hash. Two hours later, the same code appears under a different hash with GitHub's green "Verified" checkmark intact. The signature didn't break because GitHub never canonicalized it before verification.
This isn't theoretical. Research posted to arXiv on July 2 demonstrates that GitHub's verification process allows attackers to rewrite commits into new hashes without invalidating signatures. If your supply chain security relies on commit hashes as immutable identifiers, you're exposed.
The Problem: Why Hash-Based Blocking Fails
GitHub doesn't normalize signatures before checking them. This means the same cryptographic signature can produce multiple valid commit hashes. An attacker who compromises a signing key once can create infinite "verified" variants of the same commit content.
The researcher, Jacob Ginesin from Cure53, reported this to GNU and Git in January and to GitHub in March. The platforms haven't fixed the underlying issue. If you're running automated security checks that block commits by hash, those checks can be easily bypassed.
This mirrors Bitcoin's early ECDSA symmetry problem, where transaction malleability let attackers change transaction IDs without invalidating signatures. Bitcoin fixed it through strict canonicalization rules. Git repositories haven't caught up.
What You Need Before Starting
Before implementing these controls, verify your current dependencies:
Infrastructure requirements:
- Git version 2.34 or later (for
receive.certNonceSeedsupport) - Access to your Git forge's webhook configuration
- A signature verification service or script with access to commit metadata
- Logging infrastructure that captures full commit objects, not just hashes
Access requirements:
- Repository admin rights to configure protected branches
- CI/CD pipeline modification permissions
- Ability to add pre-receive hooks on your Git server
Documentation you'll need:
- Your current commit signing policy
- List of authorized signing keys and their owners
- Incident response runbook for compromised keys
If you're using GitHub Enterprise Server, you'll need version 3.9 or later to access the commit signature API endpoints programmatically.
Step-by-Step Implementation
1. Stop Trusting Hashes as Security Boundaries
Remove any security controls that use commit hashes as the sole identifier for blocking malicious code. This includes:
# Don't do this anymore
git config --global receive.denyNonFastForwards true
git config --global receive.denyDeletes true
These settings assume hash immutability. They won't catch rewritten commits with identical content.
2. Implement Content-Based Verification
Create a pre-receive hook that validates commit content, not just signatures:
#!/bin/bash
# pre-receive hook: verify-commit-content.sh
while read oldrev newrev refname; do
# Extract commit tree hash (represents actual content)
tree_hash=$(git cat-file commit "$newrev" | grep '^tree' | cut -d' ' -f2)
# Check against known malicious content trees
if grep -q "$tree_hash" /etc/git/blocked-trees.txt; then
echo "Blocked: commit content matches known malicious tree"
exit 1
fi
done
Store this in .git/hooks/pre-receive on your Git server. The tree hash represents the actual file content and can't be changed without altering the code itself.
3. Canonicalize Signatures in Your Verification Pipeline
If you're running your own verification service, add canonicalization before checking signatures. For S/MIME signatures (one of the schemes affected), this means:
from cryptography.hazmat.primitives import serialization
from cryptography import x509
def canonicalize_signature(raw_signature):
# Parse the signature
sig = x509.load_pem_x509_certificate(raw_signature.encode())
# Re-serialize in canonical DER format
canonical = sig.public_bytes(serialization.Encoding.DER)
return canonical
# Use canonical form for all verification checks
canonical_sig = canonicalize_signature(commit_signature)
verify_signature(canonical_sig, commit_content, public_key)
This prevents multiple signature representations from passing verification.
4. Implement Key Rotation Monitoring
Since signature malleability extends the impact of a compromised key, you need faster key rotation detection:
# Daily check for unexpected signing keys
git log --show-signature --since="24 hours ago" | \
grep "key ID" | \
sort -u | \
comm -13 authorized-keys.txt - > unexpected-keys.txt
if [ -s unexpected-keys.txt ]; then
# Alert security team
mail -s "Unauthorized signing keys detected" [email protected] < unexpected-keys.txt
fi
Run this as a daily cron job on your Git server.
5. Update Your Branch Protection Rules
Configure protected branches to require signature verification AND status checks:
In GitHub: Settings → Branches → Branch protection rules
- Enable "Require signed commits"
- Enable "Require status checks to pass before merging"
- Add a status check that validates tree hashes against your blocklist
Don't rely on the "Verified" badge alone. It confirms the signature is valid but doesn't prevent malleability attacks.
Validation: How to Verify It Works
Test your implementation with a controlled experiment:
- Create a test commit and sign it
- Extract the signature and manually modify non-canonical fields (padding, encoding)
- Rewrite the commit with the modified signature
- Attempt to push both versions
Your pre-receive hook should block the rewritten version based on tree hash matching, not signature validation.
Check your logs for entries like:
Blocked: commit content matches known malicious tree
Tree hash: a3f2b8c9d1e0f7a6b5c4d3e2f1a0b9c8d7e6f5a4
If you see commits passing through with different hashes but identical tree hashes, your content-based verification isn't working.
Maintenance and Ongoing Tasks
Weekly:
- Review commits signed by keys not in your authorized list
- Update your blocked-trees.txt with any newly identified malicious content
- Verify that your canonicalization function handles new signature schemes
Monthly:
- Audit your authorized signing keys list
- Test your pre-receive hooks with synthetic malicious commits
- Review false positive rates from content-based blocking
After any key compromise:
- Add all tree hashes signed by the compromised key to your blocklist
- Force-rotate to new keys across all developers
- Scan your repository history for commits signed by the compromised key, regardless of hash
The core issue won't be fixed until Git forges implement mandatory signature canonicalization. Until then, you're responsible for building these controls yourself. The alternative is assuming that a green "Verified" badge means the commit you're looking at is the only valid version of that signature, which it demonstrably isn't.



