Cryptographic Records Registry
Tamper-evident public records registry where every correction is appended and provable instead of silently overwritten.
At a glance
- Append-only record history with cryptographic chaining
- Provable corrections instead of silent overwrites
- Document storage in MinIO/S3 alongside the chain
- PostgreSQL-backed registry with audit queries
- Verification path for third parties
From the repository
README
A government-grade civic ledger where land titles, permits and vital certificates are append-only by database trigger, anchored into RFC 6962 Merkle trees, signed by an Ed25519 registry authority — and provably verifiable by any citizen, offline, with zero trust in the registry.
Features • Architecture • Workflow • Tech Stack • Quick Start • API • Verifying a Document
Public registries fail in the quiet places: a deed "corrected" after a dispute, a permit that appears in one archive and not another, a record whose history cannot be proven because the database admin could always edit the table. This ledger makes that class of tampering cryptographically visible. Every registered document becomes a leaf in a domain-separated Merkle tree; each epoch's root is sealed with the registry's Ed25519 key; and anyone holding the original document plus a small JSON proof can independently recompute the root — no database, no network, no trust required.
✨ Core Features
- 🗄️ True append-only storage —
UPDATE,DELETEandTRUNCATEon records raiseMUTATION_FORBIDDENat the PostgreSQL engine level, and anchored Merkle roots are frozen. Amending a record means issuing a superseding registration that links to the predecessor's hash — never editing history. - 🌳 RFC 6962 Merkle anchoring — domain-separated hashing (
0x00leaf /0x01node) with length-prefixed canonical preimages blocks second-preimage and preimage-collision attacks. Audit paths areO(log N). - 🔏 Ed25519-signed batch roots — every sealed epoch publishes a Merkle root signed over a domain-separated envelope, so a root signature can never be replayed as a signature over any other message.
- 🧍 Zero-knowledge citizen verification — a citizen verifies their own deed with only: the raw file, an audit path, the signed root, and the authority public key. The verification algorithm reads nothing from the registry and is byte-identical in the server, the CLI, and any reimplementation.
- 📦 Content-addressed archival — physical documents live in S3/MinIO keyed by
urn:sha256:<digest>; every retrieval re-hashes the bytes and rejects drift between key and content. - 🕐 Epoch auto-anchoring — a daemon seals batches by size (default 1,000 leaves) or interval, with lone partial epochs always sealing so records never sit unanchored.
🏗️ System Architecture

🔍 Interactive version (zoom, lens, path tracing, theme switch, PNG/SVG export): docs/architecture.html — generated with Archify and passing its schema, provenance and real-browser readability gates.
Component map
| Component | Trust zone | Responsibility | Source |
|---|---|---|---|
| Registry API | Registry | chi HTTP surface: registration intake, proof issuance, public verify endpoint | cmd/registry_server · handlers.go |
| Document Archive | Registry | S3/MinIO object store keyed by urn:sha256:<digest>; verifies digest on read | s3_archiver.go |
| Public Records Ledger | Registry | PostgreSQL 16 public_records: one immutable row per registration | record_repo.go |
| Epoch Batches | Registry | registry_epochs lifecycle OPEN → ANCHORED, leaf counters, audit log | epoch_repo.go |
| RFC 6962 Crypto Core | Registry | Domain-separated hashing, binary Merkle tree, audit-path generator | hasher.go · tree.go · proof_generator.go |
| Epoch Anchorer | Registry | Daemon that reads an epoch's leaves, builds the tree, signs the root and commits ANCHORED atomically | cmd/epoch_anchorer · sealer.go |
| Authority Key | 🔐 Private | Ed25519 keypair (PKCS#8 PEM, 0600) — the signing trust anchor | signer.go · cmd/keygen |
| Offline Verifier | 🌍 Public | Recomputes document → leaf → root from raw bytes and compares in constant time | verifier.go |
| Citizen CLI | 🌍 Public | Standalone verification tool — no network, no DB, no credentials | cmd/verify_cli |
Trust boundaries — read them carefully
- Inside the registry boundary: the API, the object store, both PostgreSQL tables and the crypto core. The registry operates these but cannot rewrite history: the append-only and anchored-root triggers block mutation, and the audit trail is itself append-only.
- The private key never leaves its box. Only the anchorer daemon reads it, and only to sign Merkle-root envelopes. It is stored as
0600-mode PKCS#8 PEM and git-ignored. - Crossing outward is deliberately minimal: an audit proof and the hex public key. The citizen zone holds no database access and no registry credentials — compromise of a citizen's machine leaks nothing; the registry can see citizens verifying nothing (the offline check never calls home).
🔄 Document Lifecycle

🔍 Interactive sequence with path tracing: docs/verification-sequence.html
- A clerk submits
POST /api/v1/records(multipart: document file +record_type+ canonical JSON metadata). - The API archives the raw bytes under
PUT urn:sha256:<digest>before any row exists — identity in the store is the content itself. - In a single
SERIALIZABLEtransaction the API locks the open epoch rowFOR UPDATE, computes the RFC 6962 leaf hash, andINSERTs the record — concurrent clerks contend on that lock and retry with jittered backoff, so leaf indices never collide. - On a timer (or batch threshold), the Epoch Anchorer reads
leaf_hash ORDER BY leaf_index— the exact, deterministic list the root must cover. - The tree is built with
0x01-prefixed interior hashing (odd trailing nodes promote unpaired); the root is signed with the Ed25519 authority key over a domain-separated envelope. - Root + signature commit atomically to
registry_epochsasANCHORED, with an entry in the append-onlyepoch_audit_log. The root is now frozen by trigger. - A citizen fetches
GET /api/v1/records/{id}/proof— anO(log N)audit path plus the signed root and public key. - The citizen runs
verify_cli(or the publicPOST /verify): signature first (the only trust anchor), then document digest from raw bytes, then leaf hash, then audit-path replay, then a constant-time root compare. - Match →
VERIFIED. One flipped byte anywhere →PROOF_REJECTED. No database query is ever made.
🧱 Tech Stack
| Layer | Technology | Role |
|---|---|---|
| Language | Go 1.27 | Single typed codebase: server, daemon, CLI, crypto |
| HTTP | go-chi/chi/v5 | Routing, request IDs, timeouts, panic recovery |
| Database | PostgreSQL 16 | Append-only ledger + epoch batches; PL/pgSQL immutability triggers |
| Object store | MinIO / S3-compatible (minio-go/v7) | Content-addressed document archive |
| Cryptography | stdlib crypto/sha256, crypto/ed25519 | RFC 6962 domain separation; Ed25519 root signatures; constant-time compare |
| Testing | stretchr/testify, go test -race | 58 unit + integration + E2E civic-audit tests |
| Ops | Docker Compose, Make | One-command local stack |
📁 Project Structure
.
├── cmd/
│ ├── registry_server/ # API gateway: intake, proofs, public verify
│ ├── epoch_anchorer/ # Batch-sealing daemon (Merkle root + Ed25519)
│ ├── keygen/ # Authority keypair generator (PKCS#8 PEM)
│ └── verify_cli/ # Standalone offline citizen verifier
├── internal/
│ ├── domain/ # PublicRecord, EpochBatch, canonical JSON
│ ├── crypto/ # hasher (RFC 6962) · signer (Ed25519) · verifier (offline)
│ ├── merkle/ # Tree construction · O(log N) audit-path generator
│ ├── anchoring/ # Epoch sealer orchestration
│ ├── storage/ # S3/MinIO content-addressed archiver
│ ├── repository/ # Append-only records · epochs · SERIALIZABLE tx pool
│ ├── registryerr/ # Sentinel errors (EPOCH_CLOSED, NOT_FOUND, …)
│ └── transport/http/ # Handlers + router
├── migrations/ # 000001_init_registry_schema.up.sql (triggers, checks, indexes)
├── tests/ # merkle · inclusion-proof · immutability · e2e civic audit
├── docs/ # 📊 Archify interactive HTML diagrams
│ └── assets/ # Static diagram exports (light + dark)
├── .archify/ # Diagram source-of-truth JSON (regenerate docs/ from here)
├── docker-compose.yml # PostgreSQL 16 + S3-compatible store
├── Makefile # up · down · migrate · keypair · build · test · clean
└── govtech_cryptographic_registry_autonomous_blueprint.md # Original spec
🚀 Quick Start
Prerequisites: Go ≥ 1.27 (go.mod pins go 1.27.0), Docker, and (for make) a POSIX shell — Git Bash / WSL on Windows, or call the go commands directly as shown below.
# 1 — Clone
git clone https://github.com/<your-username>/cryptographic-public-records-ledger.git
cd cryptographic-public-records-ledger
# 2 — Bring up the data plane (PostgreSQL 16 on :15432, S3 store on :9010)
docker compose up -d
# 3 — Generate the registry authority keypair (.keys/ is git-ignored)
go run ./cmd/keygen
# or: make keypair
# 4 — Start the registry API (reads defaults; no env needed for local dev)
go run ./cmd/registry_server
# 5 — In a second terminal: seal epochs on a timer, or --once style
EPOCH_BATCH_SIZE=10 go run ./cmd/epoch_anchorer # PowerShell: $env:EPOCH_BATCH_SIZE=10; go run ...
Register a land title and anchor it end-to-end:
# Upload a document (multipart: file + type + canonical metadata JSON)
curl -sS -X POST http://localhost:8080/api/v1/records \
-F "document=@deed.pdf" \
-F "record_type=LAND_TITLE" \
-F 'metadata={"parcel_id":"PL-7742","parties":["A. Owner"],"jurisdiction":"Metro County"}'
# → {"record_id":"<uuid>","epoch_id":1,"leaf_index":0,"leaf_hash":"...","status":"REGISTERED_PENDING_ANCHOR"}
# Seal the epoch (one pass, seal whatever is pending)
EPOCH_RUN_ONCE=true EPOCH_BATCH_SIZE=0 go run ./cmd/epoch_anchorer
# Fetch the inclusion proof
curl -sS "http://localhost:8080/api/v1/records/<record_id>/proof" -o proof.json
🧍 Citizen Offline Verification
This is the whole point of the system — verification needs no registry access:
go run ./cmd/verify_cli -document deed.pdf -proof proof.json
# VERIFIED
# record_id: 7d1c...
# document: a3f1... (sha-256)
# leaf_hash: 9be0...
# merkle_root: c44d...
Tamper with one byte and the recomputed root no longer matches the signed root:
cp deed.pdf deed-tampered.pdf && printf 'X' >> deed-tampered.pdf
go run ./cmd/verify_cli -document deed-tampered.pdf -proof proof.json
# VERIFICATION FAILED: PROOF_REJECTED: recomputed root ... does not equal signed root ...
The identical algorithm is exposed as POST /api/v1/verify for tooling that can't run the CLI — it also performs zero database reads, by construction.
📡 API Reference
| Method | Path | Purpose | Success |
|---|---|---|---|
POST | /api/v1/records | Multipart registration (document, record_type, metadata JSON) | 201 REGISTERED_PENDING_ANCHOR |
GET | /api/v1/records/{id}/proof | Audit path + signed root; PENDING_ANCHOR until sealed | 200 |
GET | /api/v1/epochs/{id} | Published SignedBatchRoot for an anchored epoch | 200 |
POST | /api/v1/verify | Offline-proof re-check, DB-free (returns verified:false as 200) | 200 |
GET | /healthz | Liveness | 200 |
Record types: LAND_TITLE · MUNICIPAL_PERMIT · BIRTH_RECORD · MARRIAGE_CERTIFICATE · BUSINESS_LICENSE.
⚙️ Configuration
| Variable | Default | Notes |
|---|---|---|
REGISTRY_ADDR | :8080 | API listen address |
REGISTRY_DSN | postgres://registry:registry@localhost:15432/civic_registry?sslmode=disable | Connection pool (25 open / 5 idle) |
AUTHORITY_KEY_PATH | .keys/authority_ed25519.key | Missing key ⇒ dev-mode ephemeral key with loud warning |
S3_ENDPOINT / S3_ACCESS_KEY / S3_SECRET_KEY / S3_BUCKET | localhost:9010 / — / — / civic-documents | Object store |
EPOCH_BATCH_SIZE / EPOCH_INTERVAL_SECONDS | 1000 / 3600 | Sealing policy (0 batch ⇒ seal everything pending) |
EPOCH_RUN_ONCE | false | Single sweep — useful for demos and CI |
S3_IMAGE (compose) | minio/minio:latest | Set to e.g. rustfs/rustfs:latest on restricted hosts; code speaks plain S3 either way |
🧪 Tests
go test ./... # unit + integration + e2e (requires `docker compose up -d`)
# or: make test # with -race
58 tests cover: known-answer Merkle vectors and domain separation · inclusion proofs for every leaf of multi-epoch trees · forged documents, broken audit paths, replayed signatures, and wrong-key rejection · UPDATE/DELETE/TRUNCATE engine-level immutability · transactional batch sealing with leaf-count mismatch guards · 50-deed end-to-end anchoring with tamper detection · concurrent registration with unique leaf assignment · content-addressed archive round-trips and digest-mismatch rejection.
🗺️ Security Model (non-negotiables)
- No mutation path: records, anchored roots and the audit log are append-only by PL/pgSQL trigger, not by convention — even
TRUNCATEis blocked. - No cross-domain hash ambiguity: leaf vs. interior nodes carry distinct
0x00/0x01prefixes; the signature envelope is domain-separated too. - No preimage ambiguity: every leaf preimage field is length-prefixed, so no two distinct records hash alike; canonical metadata bytes are stored verbatim because PostgreSQL
JSONBreorders keys. - No timing leaks on the final compare: root equality uses
crypto/subtle.ConstantTimeCompare. - Offline ≠ weakened: the verifier's step order (signature → digest → leaf → path replay → compare) means arithmetic only ever has to agree with what the authority already signed.
🛠️ Regenerating the Diagrams
Both docs/*.html render from JSON sources in .archify/ via Archify:
node <archify>/bin/archify.mjs finalize architecture .archify/architecture-registry-*/candidate.json docs/architecture.html --quality showcase
node <archify>/bin/archify.mjs finalize sequence .archify/sequence-anchor-verify-*/candidate.json docs/verification-sequence.html --quality showcase
🤝 Contributing
Fork → branch → docker compose up -d && go test ./... → open a PR. Changes to the crypto core or the immutability migration need matching proof tests — the E2E civic-audit suite is the contract.
Built for the public-interest stack that can't afford silent history edits. 🇳🇬