git-ratchet

Cryptographic forward-only protection for Git refs

Ben Birt · project-oak/git-ratchet

Oak Team · July 2026
The Problem

The Gap in Git's Integrity Model

What is a Git ref? A named pointer in a key-value store — a name like refs/heads/main maps to an object hash. Branches and tags are both refs.

✓ What Git gives you

  • Commits form a Merkle DAG — each hash covers its parents, tree, and content
  • Given a trusted hash, you can verify everything below it
  • Individual objects are self-authenticating

✗ What Git does not give you

  • Refs are mutable pointers — outside the DAG
  • Force-push silently replaces history
  • Tags can be moved to point at a different commit
  • Once old objects are GC'd, no evidence remains

Git provides integrity verification given a hash you trust — but no mechanism to establish or maintain that trust.

Motivation

A Concrete Threat

1

Gain push access

Compromised CI credential, stolen deploy key, malicious insider

2

Commit malicious code, move tag

Backdoored commit on main, v2.1.0 tag moved to point at it

3

Wait for victims

Downstream consumers pull "the same version" — silently compromised

4

Revert and erase traces

Force-push to remove malicious commits, move tag back — repo looks clean, no evidence remains

Once the traces are cleaned up, it's very difficult to detect that anything happened. Git itself has no record that the refs ever pointed anywhere else.

Overview

What git-ratchet Does

📝

Log

Append an entry:
ref → object hash

👁️

Checkpoint

Witnesses cosign
the log grew by appending

Verify

Anyone walks
the witnessed log

Branches: forward-only ratchet

Every logged state must descend from the one before it. History can only move forward.

Tags: immutable pin

Once logged, a tag is pinned to its object hash. A second object for the same tag is rejected.

Both invariants are enforced at verification, not by the witness — see slide 8.

Format

Checkpoint Format

A C2SP tlog-checkpoint over the log — not over any one ref:

// Body: origin, tree size, Merkle root hash github.com/example/repo 42 Ux7DqA1kLZ0mJt9vQhBZ3nR8sYcW2FpKdE6TgNvXjHo= // Log signature (Ed25519 or ML-DSA-44) — example-origin+a1b2c3d4 <base64 signature> // Witness cosignature(s) — witness1+e5f6a7b8 <base64 cosignature> — witness2+c9d0e1f2 <base64 cosignature>

No ref path, no commit hash, nothing Git-specific — so any tlog-witness implementation can cosign one. The refs live in the log's entries, which stay in the repository.

Foundations

How Commit Hashes Work

A commit SHA is the hash of the commit object body:

tree a3f2... ← covers every file & dir parent b7c1... ← parent commit hash author Alice <...> 1719993600 +0000 committer Alice <...> 1719993600 +0000 Fix the widget cache
↓ SHA-1 / SHA-256
e4d9a1...

Merkle chain

Each parent hash is computed the same way, so every commit hash transitively covers the entire history below it.

Self-authenticating

Given the raw bytes of a commit object, you can independently compute its hash. No trust in the sender required.

Why not verify the tree?

A verifier doesn't need to separately check the tree hash — checking out a commit inherently verifies its tree, since Git resolves every object by its content-addressed hash.

Key Mechanism

Where the Ratchet Is Enforced

The witness attests append-only growth. verify establishes descent — locally.

latest logged
e4d9a1...
logged state
b7c1f3...
logged state
93ae02...
first logged
a0ff87...

Walk the logged states

Each must descend from the one before — local merge-base calls, nothing to fetch

Objects prove themselves

Commit hashes cover their parents, so the repository can establish descent to itself

Always, never assumed

A verifier cannot know whether anything checked the log first — an attacker writing to the log ref did not

Implementations

Witness Types

HTTP Witness

Any witness on the existing network

  • Speaks C2SP tlog-witness — nothing of ours to deploy
  • Real-time cosigning via HTTP POST
  • Low latency (~ms)
  • Client is transparency-dev's, tested against their witness

Best for: witness diversity, automated pipelines

GitHub Issue Witness

Zero infrastructure (git-witness)

  • Origin opens an Issue carrying the request
  • Actions workflow cosigns + replies in a comment
  • State committed to the witness repo
  • Full audit trail in Git history

Best for: open-source projects, getting started

Same protocol either way: request and response are HTTP messages, and an issue plus a comment simply replace the POST and its reply.

Verification

What Does Verification Mean?

git-ratchet verify answers: is this ref covered by a witnessed log that only moved forward?

1

Read the checkpoint

The log's stored checkpoint, from refs/ratchet/log

2

Verify signatures + quorum

Log signature and witness cosignatures against the policy (e.g. 2 of 3)

3

Walk the logged entries

Each state descends from the last; no tag logged at a second object

4

Confirm ref state

Live ref is not ahead of what the log covers

git-ratchet verify \ --policy policy.txt \ --ref refs/heads/main

Anyone can verify

No special access needed — just the policy file and a clone of the repo.

Only the witnessed prefix counts

Entries appended past the last cosigned checkpoint are not yet witnessed — the next checkpoint covers them.

In Practice

Self-Witnessing & Getting Started

🔄 git-ratchet protects itself

Every push to main and every v* tag logs the ref, then checkpoints the log → GitHub Issue witness at BenBirt/git-witness

git-ratchet verify \ --policy ratchet-checkpoint.policy \ --ref refs/heads/main

Logging and checkpointing are separate on purpose: the entry is pushed before anyone is asked to cosign it, so a witness being down cannot cost you the record.

GitHub Actions

1

actions/setup

Install git-ratchet from a GitHub Release

2

actions/log

Origin-side: record a ref, push the entry

3

actions/checkpoint

Origin-side: get the log's head cosigned, push it

4

actions/cosign

Witness-side: answer an add-checkpoint request

Looking Ahead

Future Directions

The witnessed-checkpoint pattern generalises to any mutable pointer → immutable content.

📦

Package registries

Pin published versions with witness cosignatures — detect silent replacement or modification of packages after publication

🔨

Build systems

Integrate verification into module fetching (e.g. Bazel) — verify witnessed checkpoints at resolution time, before building

github.com/project-oak/git-ratchet  ·  c2sp.org/signed-note  ·  c2sp.org/tlog-cosignature

Speaker Notes · press N to toggle