Treeship
Get started

Merkle proofs

How Treeship ensures your receipt chain is tamper-evident and complete.

Treeship receipt chains are tamper-evident by construction. Every artifact is content-addressed, signed, and linked to the previous artifact. Breaking any link changes all subsequent IDs. This page explains each layer of the integrity model.

Hash chaining

Every receipt references the previous receipt's artifact ID via parent_id. This creates an ordered, append-only chain.

art_a1b2c3... (approval)
  <- art_d4e5f6... (action, parent_id = art_a1b2c3...)
    <- art_g7h8i9... (confirmation, parent_id = art_d4e5f6...)

If someone modifies the approval artifact, its ID changes. The action artifact still references the old ID. The chain breaks. treeship verify catches this immediately.

Content-addressed IDs

Artifact IDs are derived from the artifact's content:

art_ + hex(sha256(PAE bytes)[:16])

The ID is not assigned. It is computed. Change one byte of the artifact and the ID changes. There is no way to modify an artifact while preserving its ID.

PAE (Pre-Authentication Encoding) is the DSSE standard for encoding payload type and payload before signing. The ID is derived from the same bytes that are signed.

This means verification is simple: recompute the ID from the content and compare. If they match, the content has not been modified.

DSSE signatures

Every receipt is wrapped in a Dead Simple Signing Envelope (DSSE). The signature covers PAE-encoded bytes:

PAE(payloadType, payload)

The signing key is Ed25519. Verification requires only the public key and the envelope. No network call, no certificate chain, no token exchange.

# Verify all signatures in the chain
treeship verify --full

The verify page at treeship.dev performs the same verification client-side via WASM. The same Rust code that signs artifacts in the CLI verifies them in the browser.

Approval nonce binding

Single-use nonces prevent approval reuse. The approval artifact contains a nonce. The action artifact must echo that nonce. Verification checks the match.

# Approval creates the nonce
treeship attest approval \
  --approver human://alice \
  --description "approve deployment to staging"
# Output includes: nonce = "n_8f3a..."

# Action must echo the nonce
treeship wrap \
  --approval-nonce n_8f3a... \
  -- kubectl apply -f staging.yaml

If an agent tries to reuse a nonce, verification fails. One approval, one action. No replay.

Rekor anchoring

When attached to the Hub, receipts are optionally anchored to Sigstore's Rekor transparency log. This provides a public, immutable timestamp: proof the receipt existed before a certain time.

Rekor anchoring adds a property that local hash chaining cannot provide on its own: third-party proof of time. A local chain proves ordering (A before B before C). A Rekor entry proves "A existed before 2026-03-28T14:30:00Z" according to a public log that Treeship does not control.

treeship verify --full

Output with Rekor anchoring:

Chain integrity .............. PASS
Signature verification ....... PASS (3/3 artifacts)
Nonce binding ................ PASS
Rekor anchor ................. PASS (entry 24658012, 2026-03-28T14:30:00Z)

Merkle checkpoints

Merkle checkpoints sign a batch of artifacts into a single tree root. A checkpoint proves a receipt was in the log at a specific point in time, even if the full log is not available.

What checkpoints provide

  • Batch integrity: a single Ed25519 signature covers thousands of artifacts
  • Inclusion proofs: prove a specific artifact was in the batch without revealing other artifacts
  • Offline verification: the proof file is self-contained -- no Hub access needed to verify

Creating checkpoints

# Create a signed checkpoint of the current tree
treeship checkpoint

# Check the current tree status
treeship merkle status

A checkpoint captures the tree root, tree size, height, timestamp, and signer key ID. The canonical form for signing is:

{index}|{root}|{tree_size}|{height}|{signer}|{signed_at}

The signature is Ed25519 over this canonical string. Both the public key and signature are base64url-encoded in the checkpoint JSON.

Generating and verifying proofs

# Generate an inclusion proof for an artifact
treeship merkle proof art_f7e6d5c4b3a2f7e6

# Verify a proof file offline
treeship merkle verify proof.json

# Publish a checkpoint to Hub
treeship merkle publish

Algorithm versioning

The Merkle tree implementation tracks algorithm versions for forward compatibility:

AlgorithmIdentifierBehavior
v1 (legacy)sha256-duplicate-lastOdd leaf counts are handled by duplicating the last leaf
v2 (current)sha256-rfc9162Odd leaf counts promote the unpaired node without hashing, matching RFC 9162 (Certificate Transparency)

New checkpoints use sha256-rfc9162 by default. The verifier accepts both algorithms and rejects unknown values. If the algorithm field is missing from a proof or checkpoint, it is treated as v1 for backward compatibility.

Checkpoint verification

Verification checks three things:

  1. Leaf hash: sha256(artifact_id) matches inclusion_proof.leaf_hash
  2. Root recomputation: walking the proof path from leaf to root produces checkpoint.root
  3. Signature: the Ed25519 signature over the canonical checkpoint string is valid
treeship verify --full

Output with Merkle verification:

Chain integrity .............. PASS
Signature verification ....... PASS (3/3 artifacts)
Nonce binding ................ PASS
Rekor anchor ................. PASS (entry 24658012, 2026-03-28T14:30:00Z)
Merkle checkpoint ............ PASS (tree_size=4096, root=sha256:7e3a...)

Verification summary

When you run treeship verify --full, these are the checks:

CheckWhat it proves
Hash chainArtifacts are ordered and unmodified
Content-addressed IDsNo artifact has been tampered with
DSSE signaturesEach artifact was signed by the claimed key
Nonce bindingEach action was authorized by a specific approval
Rekor anchorThe chain existed before a public timestamp
Merkle checkpointThe artifact was included in a signed tree at a specific point in time