---
title: Merkle proofs
description: How Treeship ensures your receipt chain is tamper-evident and complete.
---

import { Callout } from 'fumadocs-ui/components/callout';

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.

<Callout type="info">
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.
</Callout>

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.

```bash
# 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 + scope

The approval artifact contains a nonce and an optional signed scope (`allowed_actors`, `allowed_actions`, `allowed_subjects`, `max_uses`). The action artifact must echo the nonce; verification then checks the binding, the scope, and the replay posture as three separate properties.

```bash
# Approval creates the nonce + scope
treeship attest approval \
  --approver human://alice \
  --description "approve deployment to staging" \
  --allowed-actor agent://deployer \
  --allowed-action deploy.staging \
  --max-uses 1
# Output includes: nonce = "n_8f3a..."

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

Verify reports each property separately:

```
✓  approval binding nonce matched a signed approval
✓  approval scope   actor / action / subject matched approval scope
⚠  replay check     package-local only -- no global ledger consulted
```

If the action's actor / action / subject falls outside the signed scope, verification fails on the scope line and the audit reader sees exactly why. Replay observation is package-local in v0.9.6; cross-package enforcement lands in v0.10 via a local Approval Use Journal (append-only hash chain with signed Merkle checkpoints), and v0.11+ adds Hub-backed checkpoints for distributed 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.

```bash
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

```bash
# 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

```bash
# 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:

| Algorithm | Identifier | Behavior |
|-----------|------------|----------|
| v1 (legacy) | `sha256-duplicate-last` | Odd leaf counts are handled by duplicating the last leaf |
| v2 (current) | `sha256-rfc9162` | Odd 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

```bash
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:

| Check | What it proves |
|-------|---------------|
| Hash chain | Artifacts are ordered and unmodified |
| Content-addressed IDs | No artifact has been tampered with |
| DSSE signatures | Each artifact was signed by the claimed key |
| Nonce binding | Each action was authorized by a specific approval |
| Rekor anchor | The chain existed before a public timestamp |
| Merkle checkpoint | The artifact was included in a signed tree at a specific point in time |
