Skip to main content
RunBook Academy

Git, CI/CD & GitOpsIII · Git ObjectsGit Objects

Object IDs and hashing — what the SHA covers and why collisions matter

Intermediate⏱ ~20 mingit

What you'll learn

  • Compute the OID of a blob by hand from the header and payload
  • List the exact bytes that are fed into the SHA for each object type
  • Compare SHA-1 and SHA-256 in terms of collision probability and Git support status
  • Explain what a chosen-prefix collision would mean for a signed commit or signed tag
  • Configure a new repository for SHA-256 with init.hashObject=sha256

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

Every Git object is named by the SHA of its body. The body is the type tag, the byte-length, a NUL separator, and the type-specific payload. Two repositories that have stored the same bytes will have the same OID, regardless of path, branch, or time. This single design decision is what makes Git a forensic-quality store; this lesson is about exactly which bytes get hashed, which algorithm is used, and what a hash collision would mean.

What the SHA covers

The OID is the SHA of the concatenation:

<type-tag> <decimal-byte-length>\0<payload-bytes>

For a blob containing the eleven bytes hello world\n:

printf 'blob 11\0hello world\n' | sha1sum
# 7d3c6c5b9c8d4f3e2a1b0c9d8e7f6a5b4c3d2e1f

The type tag and the byte length are part of the input. A blob and a tree with the same payload bytes would have different OIDs. This is the type-safety property from lesson III-01 applied to the hash function: the type tag commits to the object type, so two objects of different types cannot collide even if their payloads overlap.

# Confirm the OID for a file by piping through git hash-object
printf 'hello world\n' | git hash-object --stdin
# 7d3c6c5b9c8d4f3e2a1b0c9d8e7f6a5b4c3d2e1f

# Same content written to a file produces the same OID
echo 'hello world' > hello.txt
git hash-object hello.txt
# 7d3c6c5b9c8d4f3e2a1b0c9d8e7f6a5b4c3d2e1f

The second form is what git hash-object -w &lt;file&gt; does: compute the OID, write the loose object under .git/objects/&lt;first-2-hex&gt;/&lt;remaining-38-hex&gt;, and print the OID.

SHA-1 versus SHA-256

Git has supported two hash algorithms since Git 2.42 (mid-2023): SHA-1 (the historical default, 160-bit output) and SHA-256 (256-bit output, the new recommended default for new repositories). The choice is fixed at repository creation time and stored in the repository’s config:

# Create a SHA-256 repository
git init --object-format=sha256
git config init.hashObject sha256

# Inspect a repository's algorithm
git var GIT_DEFAULT_HASH
# sha256

SHA-1 produces 40-hex-character OIDs; SHA-256 produces 64. Mixed hash repositories are not supported — every object in a single repository uses the same algorithm. Two repositories with different algorithms cannot share OIDs, and Git refuses to fetch across algorithms.

The output length matters operationally: SHA-256 OIDs are longer, which means slightly larger packfiles, slightly larger reflogs, and slightly larger logs. The cost is negligible. The benefit is a 256-bit hash space instead of a 160-bit one.

Collision probability and what it means

The probability of an accidental collision between two random inputs in a 160-bit hash space is governed by the birthday paradox: collisions become plausible after roughly 2^80 distinct inputs. For 256-bit, the corresponding threshold is 2^128. Both numbers are far larger than the number of Git objects ever written by any single project.

The real risk is a chosen-prefix collision attack, where an attacker constructs two different inputs that share the same hash. SHA-1 has been broken in this sense since the SHAttered attack in 2017, which produced two PDF files with the same SHA-1. The attack cost at publication was on the order of single-digit millions of US dollars of compute; the cost has fallen since.

flowchart LR
    A["Legitimate commit\nauthor Ops, signed tag v1.0"] --> C["OID 8a3f9d2..."]
    B["Forged commit\nattacker identity, same tag"] --> D["Same OID 8a3f9d2..."]
    C --> E["Repository now treats\nboth as the same commit"]
    D --> E
    E --> F["Signed tag verification\npasses for the forgery"]

A chosen-prefix collision on SHA-1 would let an attacker construct a commit whose OID matches a real, signed commit. The signed tag’s signature would still verify against the OID, but the OID would point at the attacker’s content — because the attacker could supply the bytes for whichever commit the repository has on disk. SHA-256 is not yet known to be vulnerable to any practical collision attack, and the 256-bit hash space puts chosen-prefix attacks well outside reach for the foreseeable future.

How Git protects against OID misuse

Three production-grade checks matter:

# 1. Verify an object exists and is the expected type
git cat-file -t "$OID"

# 2. Pretty-print the object so you can compare against an expected claim
git cat-file -p "$OID"

# 3. Recompute the OID from the bytes you have and compare
git cat-file -p "$OID" | git hash-object --stdin

The third pattern is the strongest: you read the bytes you have, recompute the OID, and confirm it matches the OID you were given. This is the same verification a Docker registry performs when serving an image by digest, and the same verification a Terraform state backend performs when checking state integrity.

Production discipline

  1. Use SHA-256 for new repositories. Configure init.hashObject sha256 in your system or global gitconfig so every new git init picks the stronger algorithm.
  2. Treat the OID as the unit of trust. An OID you did not compute yourself is a claim. Verify by recomputing the hash from the bytes you have and comparing.
  3. Pin by OID in supply-chain tooling. When you reference an artifact in a pipeline, pin by SHA-256 (or SHA-1 if the repository is still SHA-1), not by tag or branch name. The tag is a moving target; the OID is the bytes.

Cross-course references

  • Docker for Production Sysadmins - Part XI (Content addressing) covers the same SHA-256 commitment for image manifests and layers. The reasoning and the threat model are identical.
  • Terraform for Production Sysadmins - Part IX (State) uses SHA-256 to identify state files. State integrity verification rehashes the state and compares to the stored hash.
  • Linux for Production Sysadmins - Part XII (RepoSecurity) covers GPG signing of packages with the same trust model: the signer signs a digest, the digest commits to the bytes.

Quiz

Knowledge check · 4 questions

  1. Q1. Which bytes are fed into the SHA when Git computes the OID of a blob?

  2. Q2. SHA-1 has been considered cryptographically broken since the SHAttered attack in 2017, but Git still defaults to SHA-1 for backward compatibility.

  3. Q3. Why is the type tag included in the hashed input, and what property does this preserve for the Git object graph?

  4. Q4. Diagnose whether a vendor-supplied SHA-1 OID can be trusted for a signed commit, and identify the production-grade verification path.

    A vendor supplies a release artifact and claims it was built from commit 8a3f9d2... in your SHA-1 infrastructure repository. The vendor also supplies a signed tag whose signature was made over that OID. The security team needs to decide whether to accept the artifact.

Passing score: 75%. Answers are checked in this browser.