Skip to main content
Locking Down Commit Signatures After MalleabilityResearch
4 min readFor Security Engineers

Locking Down Commit Signatures After Malleability

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.certNonceSeed support)
  • 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:

  1. Create a test commit and sign it
  2. Extract the signature and manually modify non-canonical fields (padding, encoding)
  3. Rewrite the commit with the modified signature
  4. 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.

Topics:Research

You Might Also Like